Skip to main content
RapidDev - Software Development Agency
v0-integrationsNext.js API Route

How to Integrate Xero with V0

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.

What you'll learn

  • How to create a Xero app and configure OAuth2 credentials for Next.js integration
  • How to implement the Xero OAuth2 authorization flow in Next.js API routes
  • How to build an accounting dashboard with V0 showing invoices, bills, and financial summaries
  • How to call the Xero accounting API for invoices, bank transactions, and financial reports
  • How to store and refresh Xero OAuth2 tokens securely in Vercel environment variables
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate15 min read60 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

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

Next.js API Route

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

1

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.

V0 Prompt

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.

2

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.

app/api/xero/auth/route.ts
1// app/api/xero/auth/route.ts
2import { NextResponse } from 'next/server';
3import { XeroClient } from 'xero-node';
4
5const 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 tokens
18 ],
19});
20
21export async function GET() {
22 const consentUrl = await xero.buildConsentUrl();
23 return NextResponse.redirect(consentUrl);
24}
25
26// app/api/xero/callback/route.ts
27// (Separate file)
28export { callbackHandler as GET } from './callback-handler';
29
30// lib/xero-callback.ts
31export 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.

3

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.

V0 Prompt

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

app/api/xero/invoices/route.ts
1// app/api/xero/invoices/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { XeroClient, Invoice } from 'xero-node';
4
5const 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});
11
12export async function GET(request: NextRequest) {
13 // In a real app, load tokens from your database or encrypted session
14 const accessToken = process.env.XERO_ACCESS_TOKEN; // For demo — use database in production
15 const refreshToken = process.env.XERO_REFRESH_TOKEN;
16 const tenantId = process.env.XERO_TENANT_ID;
17
18 if (!accessToken || !tenantId) {
19 return NextResponse.json({ error: 'Not authenticated with Xero', requiresAuth: true }, { status: 401 });
20 }
21
22 try {
23 // Set the token on the xero-node client
24 xero.setTokenSet({ access_token: accessToken, refresh_token: refreshToken, token_type: 'Bearer' });
25
26 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);
29
30 const response = await xero.accountingApi.getInvoices(
31 tenantId,
32 undefined, // ifModifiedSince
33 undefined, // where
34 'DueDateASC', // order
35 undefined, // IDs
36 undefined, // invoiceNumbers
37 undefined, // contactIDs
38 statuses,
39 page,
40 false, // includeArchived
41 true, // createdByMyApp - show all invoices
42 undefined,
43 undefined,
44 100 // unitdp - decimal places
45 );
46
47 const invoices = response.body.invoices || [];
48
49 // Calculate summary statistics
50 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 logic
54 currency: invoices[0]?.currencyCode || 'USD',
55 };
56
57 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.

4

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.

V0 Prompt

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.

V0 Prompt

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.

V0 Prompt

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.

typescript
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 storedRefreshToken
10 );
11 await saveNewTokens(newTokenSet); // Save to database
12 return await callXeroApi(); // Retry with new token
13 }
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

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.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

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.