To integrate Xero with V0 by Vercel, generate an accounting dashboard UI with V0, implement Xero OAuth2 authentication in Next.js API routes, store your Xero credentials in Vercel environment variables, and deploy. Your app can display invoices, bills, bank reconciliation status, and financial reports for UK, Australian, and New Zealand businesses without exposing credentials to the browser.
Build Accounting Dashboards and Automate Xero Workflows with V0 and Next.js
Xero is the dominant accounting platform for small and medium businesses in the UK, Australia, and New Zealand, and has strong adoption in many other countries. While Xero's own interface is comprehensive for accountants and bookkeepers, businesses frequently need custom integrations — client-facing invoice portals, automated financial reporting dashboards for managers who don't need full Xero access, or integrations that push data into Xero from external systems like e-commerce platforms or CRM tools. V0 makes it fast to generate the front-end for these custom accounting workflows.
Xero's API is a mature REST API with comprehensive coverage: invoices, bills, bank transactions, bank reconciliation, contacts, accounts, expense claims, purchase orders, payroll, projects, and detailed financial reports (P&L, balance sheet, cash flow). Authentication is OAuth2 — users authorize your app to access their Xero organization, and you receive access and refresh tokens. The xero-node SDK abstracts token management and provides typed methods for every API endpoint, which is the recommended integration approach for Next.js apps.
Xero's multi-tenancy model is important to understand: a single user can be connected to multiple Xero organizations (tenants), and API calls must specify which tenant's data to access via the Xero-Tenant-Id header. After OAuth2 authentication, your app calls the connections endpoint to get the list of organizations the user has authorized, and stores the tenant ID alongside the tokens for subsequent API calls.
Integration method
Xero integrates with V0-generated Next.js apps through OAuth2 authentication and REST API routes. Your Xero app credentials (Client ID and Client Secret) are stored as server-only Vercel environment variables. Users connect their Xero account through an OAuth2 authorization flow, and your API routes use the resulting access token to call Xero's accounting API. The xero-node SDK simplifies token management and API calls. For server-side-only integrations like automated reporting, the client_credentials flow is available for Xero OAuth2 without user interaction.
Prerequisites
- A Xero developer account — sign up at developer.xero.com (separate from your Xero accounting account)
- A Xero app created at developer.xero.com/app/manage with OAuth2 credentials — note your Client ID and Client Secret
- A redirect URI configured in your Xero app settings — must exactly match the URL Vercel assigns (e.g., https://your-app.vercel.app/api/xero/callback)
- At least one Xero accounting organization connected to your Xero account for testing
- A V0 account at v0.dev for generating the dashboard UI and a Vercel account for deployment
Step-by-step guide
Generate the Accounting Dashboard UI with V0
Generate the Accounting Dashboard UI with V0
Open V0 at v0.dev and describe the financial dashboard interface you want to build. Accounting dashboards benefit from clear visual hierarchy: key financial figures prominently displayed, color-coded status indicators (red for overdue, green for paid, blue for pending), and tables that support sorting by amount, date, or status. When prompting V0, be specific about the Xero data entities you want to display — invoices, bank balances, contacts, or financial report summaries. V0 generates React components with shadcn/ui — Table, Card, Badge, and Chart components from recharts work well for financial dashboards. Describe the currency format you need (Xero supports multi-currency, so specify whether amounts should be shown in the account currency or a specific currency), and ask V0 to include appropriate number formatting for financial figures. Include the API endpoint paths your components will call (/api/xero/invoices, /api/xero/bank-summary) so V0 generates accurate fetch calls. For dashboards that require user authentication with their own Xero account, also ask V0 to include a 'Connect Xero Account' button that links to /api/xero/auth — this starts the OAuth2 flow. Push the generated code to GitHub via V0's Git panel.
Build a Xero accounting overview page with a top section showing three KPI cards: Outstanding Receivables (blue), Overdue Invoices (red, count and amount), and Cash in Bank (green, total across all accounts). Below, show two side-by-side panels: an Invoices panel with a table of recent invoices (contact name, invoice number, due date, amount, status badge) and a Bank Accounts panel listing each account with name and current balance. Add a 'Connect to Xero' button in the top right if not authenticated. Load data from /api/xero/dashboard. Professional accounting app design with subtle gray borders and clear number formatting.
Paste this in V0 chat
Pro tip: Ask V0 to use Intl.NumberFormat for currency display in financial components — specify the currency code (GBP, AUD, NZD, USD) and locale so amounts format correctly for your target market.
Expected result: A Xero accounting dashboard renders in V0's preview with KPI cards, invoice table, and bank account list. The components reference /api/xero/dashboard for data and include a Xero connection state handler.
Implement Xero OAuth2 Authentication
Implement Xero OAuth2 Authentication
Xero requires OAuth2 authentication, which involves redirecting users to Xero's authorization page and handling the callback with the authorization code. Install the xero-node SDK with npm install xero-node — it handles the OAuth2 flow, token refresh, and all API calls. The OAuth2 flow has three steps: first, your app redirects the user to Xero's authorization URL with your client ID, scopes, and a redirect URI; second, the user logs into Xero and approves the connection, and Xero redirects back to your callback URL with an authorization code; third, your callback route exchanges the code for access and refresh tokens. Create two API routes: /api/xero/auth (initiates the OAuth2 flow by building the authorization URL and redirecting) and /api/xero/callback (handles the return from Xero, exchanges the code for tokens, and stores them). In production, store the refresh token in a database or encrypted cookie so you can refresh access tokens without requiring re-authentication. For development and single-user apps, you can store the token in an encrypted HTTP-only cookie. The access token expires after 30 minutes — use the xero-node client's refreshToken() method when the access token expires. The refresh token is valid for 60 days and itself refreshes with each use.
1// app/api/xero/auth/route.ts2import { NextResponse } from 'next/server';3import { XeroClient } from 'xero-node';45const xero = new XeroClient({6 clientId: process.env.XERO_CLIENT_ID!,7 clientSecret: process.env.XERO_CLIENT_SECRET!,8 redirectUris: [process.env.XERO_REDIRECT_URI!],9 scopes: [10 'openid',11 'profile',12 'email',13 'accounting.transactions',14 'accounting.contacts',15 'accounting.reports.read',16 'accounting.settings',17 'offline_access', // Required for refresh tokens18 ],19});2021export async function GET() {22 const consentUrl = await xero.buildConsentUrl();23 return NextResponse.redirect(consentUrl);24}2526// app/api/xero/callback/route.ts27// (Separate file)28export { callbackHandler as GET } from './callback-handler';2930// lib/xero-callback.ts31export async function getXeroCallbackHandler() {32 const { XeroClient } = await import('xero-node');33 return new XeroClient({34 clientId: process.env.XERO_CLIENT_ID!,35 clientSecret: process.env.XERO_CLIENT_SECRET!,36 redirectUris: [process.env.XERO_REDIRECT_URI!],37 scopes: ['openid', 'profile', 'email', 'accounting.transactions', 'accounting.contacts', 'accounting.reports.read', 'offline_access'],38 });39}Pro tip: Always include offline_access in your Xero OAuth2 scopes — without it, Xero does not issue a refresh token, meaning users must re-authorize your app every 30 minutes when their access token expires.
Expected result: Visiting /api/xero/auth redirects the user to Xero's login and authorization page. After approving the connection, they're redirected back to your app's callback URL with an authorization code ready to be exchanged for tokens.
Create Xero API Routes for Financial Data
Create Xero API Routes for Financial Data
With authentication in place, create API routes that call Xero's accounting API for your dashboard data. The xero-node SDK provides typed methods for all Xero API endpoints. A critical requirement for all Xero API calls is the Xero Tenant ID — after OAuth2 authentication, call xero.updateTenants() to get the list of organizations the user has connected, then include the tenantId in every subsequent API call. Store the tenant ID in your session or cookie alongside the access token. For invoices, use the xero.accountingApi.getInvoices() method which accepts filter parameters for status (DRAFT, SUBMITTED, AUTHORISED, VOIDED, DELETED — 'AUTHORISED' means approved and outstanding, not authorized in a permissions sense), contact ID, date range, and page number. For bank accounts and balances, call xero.accountingApi.getAccounts() with type filter BANK to list bank accounts, then xero.accountingApi.getBankTransactions() for transaction history. For financial reports (P&L, Balance Sheet), use xero.accountingApi.getReportProfitAndLoss() and xero.accountingApi.getReportBalanceSheet() which accept reporting date parameters. Parse the report responses carefully — Xero's report API returns a hierarchical data structure with Rows containing nested Cells, which requires recursive traversal to extract the figures you need for your dashboard.
Add a financial reports section to the dashboard showing a 6-month P&L summary chart. The chart is a grouped bar chart with two bars per month — Revenue (blue) and Expenses (red). Below the chart, show a summary table with month names and the revenue, expense, and net profit figures. Load from /api/xero/financial-summary and show a loading skeleton while data fetches. Include a year selector dropdown in the section header. Show values formatted as currency with comma separators and the appropriate currency symbol.
Paste this in V0 chat
1// app/api/xero/invoices/route.ts2import { NextRequest, NextResponse } from 'next/server';3import { XeroClient, Invoice } from 'xero-node';45const xero = new XeroClient({6 clientId: process.env.XERO_CLIENT_ID!,7 clientSecret: process.env.XERO_CLIENT_SECRET!,8 redirectUris: [process.env.XERO_REDIRECT_URI!],9 scopes: [],10});1112export async function GET(request: NextRequest) {13 // In a real app, load tokens from your database or encrypted session14 const accessToken = process.env.XERO_ACCESS_TOKEN; // For demo — use database in production15 const refreshToken = process.env.XERO_REFRESH_TOKEN;16 const tenantId = process.env.XERO_TENANT_ID;1718 if (!accessToken || !tenantId) {19 return NextResponse.json({ error: 'Not authenticated with Xero', requiresAuth: true }, { status: 401 });20 }2122 try {23 // Set the token on the xero-node client24 xero.setTokenSet({ access_token: accessToken, refresh_token: refreshToken, token_type: 'Bearer' });2526 const { searchParams } = new URL(request.url);27 const statuses = (searchParams.get('statuses') || 'AUTHORISED,OVERDUE').split(',') as Invoice['status'][];28 const page = parseInt(searchParams.get('page') || '1', 10);2930 const response = await xero.accountingApi.getInvoices(31 tenantId,32 undefined, // ifModifiedSince33 undefined, // where34 'DueDateASC', // order35 undefined, // IDs36 undefined, // invoiceNumbers37 undefined, // contactIDs38 statuses,39 page,40 false, // includeArchived41 true, // createdByMyApp - show all invoices42 undefined,43 undefined,44 100 // unitdp - decimal places45 );4647 const invoices = response.body.invoices || [];4849 // Calculate summary statistics50 const summary = {51 total: invoices.length,52 totalOutstanding: invoices.reduce((sum, inv) => sum + (inv.amountDue || 0), 0),53 overdueCount: invoices.filter(inv => inv.isDiscounted).length, // Use your overdue logic54 currency: invoices[0]?.currencyCode || 'USD',55 };5657 return NextResponse.json({ invoices, summary });58 } catch (error) {59 const message = error instanceof Error ? error.message : 'Unknown error';60 console.error('Xero invoices fetch failed:', message);61 return NextResponse.json({ error: message }, { status: 500 });62 }63}Pro tip: Xero's access tokens expire after 30 minutes. Wrap your xero-node API calls in a try-catch that detects token expiry errors (HTTP 401) and automatically refreshes the token using xero.refreshWithRefreshToken(clientId, clientSecret, refreshToken) before retrying the original request.
Expected result: GET /api/xero/invoices returns a list of outstanding invoices with amounts, due dates, contact names, and status information from the connected Xero organization, along with summary statistics for the dashboard KPI cards.
Configure Vercel Environment Variables and Deploy
Configure Vercel Environment Variables and Deploy
Configure all Xero credentials in Vercel before deploying. Open the Vercel Dashboard, navigate to your project, and go to Settings → Environment Variables. Add XERO_CLIENT_ID with your Xero app's Client ID from developer.xero.com/app/manage. Add XERO_CLIENT_SECRET with the Client Secret — treat this as a password. Add XERO_REDIRECT_URI with the exact redirect URI you configured in your Xero app — this must match character-for-character, including trailing slashes. For Vercel deployments, the URI will be https://your-project.vercel.app/api/xero/callback. Do not use NEXT_PUBLIC_ on any of these variables. For testing with real tokens, also add XERO_ACCESS_TOKEN, XERO_REFRESH_TOKEN, and XERO_TENANT_ID if you're using the simple environment variable token storage (for production, use a database for token storage instead). Set all variables for Production and Preview environments. After deploying, test the OAuth2 flow by visiting your live URL, clicking 'Connect to Xero', completing the authorization on Xero's site, and verifying you're redirected back to your dashboard with data loading. Important: In production apps, store Xero tokens in a database (Supabase, Neon) rather than environment variables — tokens change on every refresh and must be updated dynamically, which is not possible with environment variables. For complex multi-tenant apps where multiple users each connect their own Xero organizations, RapidDev's team can help implement the full token storage and multi-tenant architecture.
Pro tip: Add /api/xero/callback to your Xero app's allowed redirect URIs in the Xero developer portal before deploying — Xero will reject the OAuth2 callback with an error if the redirect URI doesn't exactly match one of the registered URIs in your app settings.
Expected result: The Vercel deployment succeeds. The Xero OAuth2 flow redirects users to Xero for authorization and back to your app. The accounting dashboard displays real invoice data, bank balances, and financial summaries from the connected Xero organization.
Common use cases
Invoice Management Dashboard
A financial dashboard showing outstanding invoices grouped by status (Draft, Awaiting Payment, Overdue, Paid), with total amounts, aging analysis, and quick action buttons to mark invoices as sent or void. Finance managers can see their receivables position at a glance without logging into Xero.
Build an invoice management dashboard with four status columns: Draft (gray), Awaiting Payment (blue), Overdue (red), and Paid (green). Each column shows total count and total amount. Invoice cards show contact name, invoice number, invoice date, due date, amount in the account currency, and days overdue as a badge if past due. Include a search input to filter by contact name and a date range filter. Show a total outstanding amount prominently at the top. Load from /api/xero/invoices. Use clean financial app styling with bold amount typography.
Copy this prompt to try it in V0
Client Billing Portal with Online Payment
A client-facing portal where customers can view their outstanding invoices from Xero and make payments via Stripe. The portal authenticates clients by email, shows only their invoices fetched via Xero's API, and creates Stripe payment sessions when they click 'Pay Now' — linking invoice payments back to Xero reconciliation.
Create a client billing portal with a header showing 'Client Invoices' and the client's company name. List open invoices in a table with columns: invoice number, issue date, due date, description, amount excluding tax, tax amount, total amount, and status badge. Include a 'Pay Now' button for unpaid invoices that triggers a Stripe checkout. Show a separate 'Payment History' section for paid invoices. Load from /api/xero/client-invoices. Professional invoice portal design with white background and subtle blue accent.
Copy this prompt to try it in V0
Monthly Financial Summary Dashboard
An executive financial overview showing monthly P&L summary (revenue vs expenses), cash position from bank accounts, accounts receivable aging, and accounts payable summary — all pulled from Xero's reporting APIs. Executives get key financial metrics without needing Xero access.
Design a monthly financial dashboard with a top row of KPI cards: Monthly Revenue, Monthly Expenses, Net Profit, and Current Cash Balance. Below, show a P&L bar chart comparing revenue and expenses by month for the last 6 months. Add a receivables aging table (0-30 days / 31-60 days / 61-90 days / 90+ days) with total amounts. Include a bank accounts balance summary showing each connected bank account name and current balance. Data loads from /api/xero/financial-summary. Use professional financial dashboard styling.
Copy this prompt to try it in V0
Troubleshooting
Xero OAuth2 returns 'Invalid redirect URI' error during authorization
Cause: The redirect URI in your API route doesn't exactly match one of the registered redirect URIs in your Xero app settings. Even a trailing slash difference causes this error.
Solution: Log in to developer.xero.com/app/manage and check the Redirect URIs configured for your app. Ensure the XERO_REDIRECT_URI environment variable matches exactly — including https vs http, trailing slash, and the full path (/api/xero/callback). Add your Vercel production URL as a redirect URI in the Xero app settings.
Xero API returns 403 Forbidden with 'AuthenticationUnsuccessful' error
Cause: The access token has expired (Xero access tokens are valid for only 30 minutes) or the tenant ID is incorrect. The xero-node SDK may not have the current token set.
Solution: Implement automatic token refresh: catch 401 errors, call xero.refreshWithRefreshToken() with the stored refresh token, save the new access and refresh tokens, and retry the failed request. If the refresh token has also expired (after 60 days of non-use), the user must re-authorize through the OAuth2 flow.
1// Token refresh pattern:2try {3 return await callXeroApi();4} catch (error) {5 if (isTokenExpiredError(error)) {6 const newTokenSet = await xero.refreshWithRefreshToken(7 process.env.XERO_CLIENT_ID!,8 process.env.XERO_CLIENT_SECRET!,9 storedRefreshToken10 );11 await saveNewTokens(newTokenSet); // Save to database12 return await callXeroApi(); // Retry with new token13 }14 throw error;15}Xero API returns 'Organisation is not subscribed to Payroll' error
Cause: You're requesting payroll data from a Xero organization that is based in a country where Xero Payroll is not available, or the organization has not activated the Payroll module.
Solution: Check whether the Xero organization's region supports the specific API endpoint you're calling. Xero's payroll API varies by country — some endpoints are specific to AU, NZ, or UK organizations. Remove payroll scopes from your OAuth2 authorization request if you don't need payroll data, and ensure your app handles cases where optional Xero modules are not activated.
xero.updateTenants() returns an empty array after OAuth2 callback
Cause: The OAuth2 callback did not complete successfully, the token was not properly set on the xero-node client, or the user's Xero account has no organizations.
Solution: Verify the callback route successfully calls xero.apiCallback(callbackUrl) with the full callback URL (including query parameters) before calling xero.updateTenants(). Log the token set returned from apiCallback to verify it contains a valid access_token. The user must have at least one Xero organization to get a non-empty tenants list.
Best practices
- Store Xero OAuth2 tokens in a database rather than environment variables in production — tokens change on every refresh (every 30 minutes) and must be updated dynamically
- Always include offline_access in Xero OAuth2 scopes to receive refresh tokens — without it users must re-authorize every 30 minutes when the access token expires
- Implement automatic token refresh with retry logic — catch 401 errors, refresh the token, save the new tokens, and retry the failed request transparently
- Request only the Xero OAuth2 scopes your app actually needs — requesting broader scopes than necessary increases security risk and may deter users from authorizing
- Always store the Xero tenant ID alongside the access token — every Xero API call requires the tenant ID header and it must match the organization the user authorized
- Never use NEXT_PUBLIC_ prefix on any Xero credentials — Client Secret and access tokens grant full access to accounting data and must remain server-only
- Add a Xero disconnection flow that revokes tokens and clears stored credentials — users expect to be able to disconnect their accounting app integration when they want to
Alternatives
Use QuickBooks instead of Xero if your target market is primarily US-based — QuickBooks dominates US small business accounting with stronger US payroll, tax forms, and local accountant network integrations than Xero.
Choose FreshBooks instead of Xero if your use case is freelancer or service business invoicing rather than full accounting — FreshBooks has a simpler API and is better suited for time-tracking and project-based billing without double-entry accounting complexity.
Use Zoho Books instead of Xero if you need a broader Zoho business suite integration at lower cost — Zoho Books connects natively with Zoho CRM, Inventory, and Desk in ways Xero requires third-party integrations to achieve.
Choose Wave instead of Xero if your use case is free small business accounting for US/Canadian businesses — Wave's accounting software and API are free, though with fewer features and integrations than Xero's paid platform.
Frequently asked questions
Do I need a separate Xero developer account from my Xero accounting account?
Yes — you need a Xero developer account at developer.xero.com to create apps and get API credentials. This is separate from your Xero accounting subscription. The developer account is free and used only for API app management. Your accounting organization connects to your app through OAuth2 during testing and production use.
How long do Xero access tokens last, and how do I handle expiry?
Xero access tokens expire after 30 minutes. Refresh tokens are valid for 60 days from their last use and rotate with each refresh — when you use a refresh token to get a new access token, you also receive a new refresh token that resets the 60-day clock. Implement automatic refresh by catching HTTP 401 errors and calling xero.refreshWithRefreshToken() before retrying the request.
Can I connect multiple Xero organizations in the same app?
Yes — Xero's multi-tenancy model supports this natively. After OAuth2 authorization, call xero.updateTenants() to get all organizations the user has authorized. Store each organization's tenant ID separately and allow users to switch between them in your UI. Each API call specifies which tenant to use via the tenantId parameter.
What scopes do I need for basic invoicing and financial reporting?
For invoices and basic financial data: accounting.transactions (read/write invoices, bills, bank transactions), accounting.contacts (read/write contacts), accounting.reports.read (P&L, balance sheet, reports), accounting.settings (read chart of accounts and settings), and offline_access (for refresh tokens). Request only the scopes you need — users see the permission list during OAuth2 authorization.
Does the Xero integration work differently for UK, Australian, and New Zealand accounts?
The core API structure is the same across regions, but some features are region-specific. UK accounts have Making Tax Digital (MTD) VAT filing endpoints. Australian accounts have GST and BAS (Business Activity Statement) endpoints. Payroll endpoints differ significantly by country. The xero-node SDK handles these regional differences — check the Xero developer documentation for which endpoints are available in each region before building region-specific features.
Can I create and send invoices through the Xero API from my V0 app?
Yes — the Xero API supports full invoice lifecycle management: creating draft invoices, submitting for approval, marking as sent, recording payments, and voiding. Use xero.accountingApi.createInvoices() for creation and xero.accountingApi.updateInvoice() for status changes. For sending invoices by email, Xero can send via their email service when you set the invoice status to SUBMITTED or call the email endpoint explicitly.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation