Integrating Xero with Lovable requires Edge Functions to proxy the Xero REST API with OAuth2 PKCE authentication. Store your Xero Client ID and Client Secret in Cloud Secrets, build an OAuth2 PKCE authorization flow, manage tenant IDs for multi-organization access, and create Edge Functions for invoices, contacts, and bank transactions. Xero is the dominant accounting platform in the UK, Australia, and New Zealand, making this integration essential for businesses in those markets.
Why integrate Xero with Lovable?
Xero has over 4 million subscribers globally and is the preferred accounting platform for businesses in the United Kingdom, Australia, New Zealand, and much of Southeast Asia. If you're building a Lovable app for clients or users in these markets, Xero is the accounting system they almost certainly use — and integrating with it means you can build client-facing portals, financial dashboards, automated invoicing workflows, and reporting tools that connect directly to their live accounting data.
Xero's API differs from QuickBooks in two notable ways. First, Xero uses OAuth2 PKCE (Proof Key for Code Exchange), a more secure variation of the OAuth2 flow that doesn't require a client secret for the initial authorization exchange — this makes the flow somewhat safer against certain attack vectors but adds a code verifier/challenge step you need to implement. Second, Xero supports multi-tenancy at the account level: a single Xero user can be an administrator across multiple organizations, and your app must specify a Xero-Tenant-Id header on every API call to indicate which organization's data to operate on.
The most popular Xero + Lovable integration patterns are invoice management portals, contractor payment dashboards, bank reconciliation helpers, and automated invoice generation triggered by app events. The Xero API covers all of these use cases through well-documented endpoints for contacts (customers/suppliers), invoices, payments, credit notes, bank transactions, and financial reports.
Integration method
Xero has no native Lovable connector. All authenticated Xero API calls run through Supabase Edge Functions using OAuth2 PKCE. Edge Functions manage token refresh, tenant ID selection for multi-organization access, and proxy requests to Xero's REST endpoints for contacts, invoices, payments, and bank transactions. Credentials and tokens are stored in Cloud Secrets and Supabase with RLS protection.
Prerequisites
- A Lovable project with Cloud enabled
- A Xero account (start with a Xero Demo Company for development — it's free and comes with sample data)
- A Xero Developer account at developer.xero.com — sign up free with your Xero account
- A Xero app created in the developer portal with OAuth2 PKCE configured
- Basic understanding of OAuth2 PKCE flow — it adds code_verifier and code_challenge parameters to the standard OAuth2 code flow
Step-by-step guide
Create a Xero Developer app and get OAuth2 credentials
Create a Xero Developer app and get OAuth2 credentials
Go to developer.xero.com and sign in with your Xero account. Click 'New app' in the developer dashboard. Give your app a name, select 'Web app' as the integration type, and enter your company URL. In the Redirect URIs field, add your Edge Function callback URL: https://[your-project].supabase.co/functions/v1/xero-oauth. Click 'Create app'. On the app settings page, you'll see your Client ID (a UUID format string). For OAuth2 PKCE apps, Xero does not display a Client Secret — PKCE replaces the client secret with a dynamically generated code verifier/challenge pair. This is one key difference from QuickBooks's OAuth2 implementation. Note: while Xero PKCE apps don't require a client secret for the authorization code exchange, server-side apps (like Edge Functions) can still optionally use a Client Secret for additional security during the token endpoint call. In the app settings, you can generate a Client Secret if you want to use it. For this tutorial, we'll include it as an optional additional security layer. In the 'Scopes' section, select the permissions your app needs. For accounting data, select 'openid', 'profile', 'email', 'accounting.transactions' (invoices, bank transactions, credit notes), 'accounting.contacts' (customers and suppliers), and 'offline_access' (required to get a refresh token for persistent access). Click 'Save'.
Pro tip: Xero's Demo Company is the best environment for development. Sign in to xero.com, and if you don't see a Demo Company option, go to xero.com/signup and look for the Demo Company offer during the trial flow. The Demo Company has populated invoices, contacts, bank accounts, and transactions for realistic testing.
Expected result: Your Xero app is created. You have the Client ID. The redirect URI and required scopes are saved. You have access to the Demo Company in your Xero account.
Store Xero credentials in Cloud Secrets and implement the OAuth2 PKCE flow
Store Xero credentials in Cloud Secrets and implement the OAuth2 PKCE flow
Open your Lovable project, click '+' → Cloud → Secrets. Add XERO_CLIENT_ID with your app's Client ID. Add XERO_REDIRECT_URI with the full Edge Function URL (https://[your-project].supabase.co/functions/v1/xero-oauth). Add XERO_SCOPES with value 'openid profile email accounting.transactions accounting.contacts offline_access'. The OAuth2 PKCE flow works as follows: your app generates a random code_verifier (a 43-128 character random string), computes a code_challenge by hashing it with SHA-256 and base64url-encoding the result, includes the code_challenge in the authorization URL, redirects the user to Xero's authorization endpoint, receives the authorization code at your redirect URI, exchanges the code (plus the original code_verifier) for access and refresh tokens at Xero's token endpoint. For a single-user setup (your own Xero account), you can complete this flow once using Xero's OAuth 2.0 Playground at developer.xero.com/documentation/oauth2/pkce-flow or manually. Store the resulting access_token (short-lived, 30 minutes), refresh_token (long-lived), and token_set expiry in Supabase's xero_tokens table. Add XERO_REFRESH_TOKEN to Cloud Secrets with the initial refresh token obtained after authorization. For multi-user apps, create a 'Connect Xero' button that triggers the PKCE flow for each user — the Edge Function generates the code verifier/challenge and redirects to Xero's authorization URL. The callback Edge Function completes the exchange and stores tokens per user.
Create a Supabase table called xero_tokens with columns: id, user_id, access_token, refresh_token, tenant_id, tenant_name, expires_at, created_at, updated_at. Add RLS so users can only read their own row. Create an Edge Function at supabase/functions/xero-oauth/index.ts that handles the OAuth2 PKCE callback: receives the authorization code from the URL query parameter, uses XERO_CLIENT_ID and XERO_REDIRECT_URI from Deno.env, exchanges the code for tokens at https://identity.xero.com/connect/token, then calls GET https://api.xero.com/connections to get the tenant list and saves the first tenant's ID as tenant_id. Store all token fields in the xero_tokens table using service role key. Redirect to /dashboard on success.
Paste this in Lovable chat
Pro tip: Unlike QuickBooks which embeds the company (realm) ID in the API URL, Xero requires a Xero-Tenant-Id header on every API request. If the user has multiple Xero organizations, you must ask them which one to use or store all tenant IDs and let them switch context in your app's settings.
Expected result: XERO_CLIENT_ID, XERO_REDIRECT_URI, and XERO_SCOPES are set in Cloud Secrets. The xero_tokens table is created in Supabase. After completing OAuth2 authorization, tokens and tenant_id are stored.
Create the Xero API proxy Edge Function with token refresh
Create the Xero API proxy Edge Function with token refresh
Build the main Edge Function that handles all Xero API operations. Like QuickBooks, Xero's access tokens expire after 30 minutes — significantly shorter than most services. Every Edge Function invocation must check if the access token is expired and refresh it before making the API call. The token refresh flow for Xero uses the standard OAuth2 refresh grant: POST to https://identity.xero.com/connect/token with grant_type=refresh_token and the stored refresh_token. The response includes a new access_token and a new refresh_token (rotate on each use — always save the latest one). Update both in the database after each refresh. Xero's REST API follows a consistent pattern: all accounting API calls go to https://api.xero.com/api.xro/2.0/{Resource} with the Xero-Tenant-Id header specifying which organization's data to access. Common endpoints: GET /Invoices (with Status filter), POST /Invoices (create), GET /Contacts, POST /Payments (record payment against invoice). Xero supports both XML and JSON. Always use JSON by including the Accept: application/json header. For list endpoints, Xero uses If-Modified-Since header-based pagination — pass the header with a datetime to get only records modified since that time, which is efficient for incremental syncs.
Create a Supabase Edge Function at supabase/functions/xero/index.ts. It should read the user's access_token, refresh_token, tenant_id, and expires_at from the xero_tokens table using service role key (identified by user_id from the Authorization JWT). If the token is expired, refresh it using https://identity.xero.com/connect/token with XERO_CLIENT_ID from Deno.env, update the stored tokens. Then support these actions: action='get-invoices' fetches GET /Invoices?Statuses=AUTHORISED,SUBMITTED from Xero API with Xero-Tenant-Id header; action='get-contacts' fetches GET /Contacts?IsSupplier=false; action='create-invoice' POSTs a new invoice to /Invoices. Return JSON for each. Add CORS headers.
Paste this in Lovable chat
1// supabase/functions/xero/index.ts2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";45const XERO_BASE = "https://api.xero.com/api.xro/2.0";6const TOKEN_URL = "https://identity.xero.com/connect/token";7const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization" };89async function refreshTokens(supabase: any, userId: string, refreshToken: string) {10 const clientId = Deno.env.get("XERO_CLIENT_ID") ?? "";11 const res = await fetch(TOKEN_URL, {12 method: "POST",13 headers: { "Content-Type": "application/x-www-form-urlencoded" },14 body: new URLSearchParams({15 grant_type: "refresh_token",16 refresh_token: refreshToken,17 client_id: clientId,18 }),19 });20 const data = await res.json();21 if (!data.access_token) throw new Error("Token refresh failed: " + JSON.stringify(data));22 const expiresAt = new Date(Date.now() + data.expires_in * 1000).toISOString();23 await supabase.from("xero_tokens").update({24 access_token: data.access_token,25 refresh_token: data.refresh_token,26 expires_at: expiresAt,27 updated_at: new Date().toISOString(),28 }).eq("user_id", userId);29 return data.access_token;30}3132serve(async (req) => {33 if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });34 try {35 const authHeader = req.headers.get("Authorization") ?? "";36 const supabase = createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "");37 const { data: { user } } = await createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_ANON_KEY") ?? "", { global: { headers: { Authorization: authHeader } } }).auth.getUser();38 const userId = user?.id;39 if (!userId) return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, headers: corsHeaders });4041 const { data: tokenRow } = await supabase.from("xero_tokens").select("*").eq("user_id", userId).single();42 if (!tokenRow) return new Response(JSON.stringify({ error: "No Xero connection found" }), { status: 404, headers: corsHeaders });4344 let accessToken = tokenRow.access_token;45 if (new Date(tokenRow.expires_at) < new Date(Date.now() + 60000)) {46 accessToken = await refreshTokens(supabase, userId, tokenRow.refresh_token);47 }4849 const headers = {50 Authorization: `Bearer ${accessToken}`,51 "Xero-Tenant-Id": tokenRow.tenant_id,52 Accept: "application/json",53 "Content-Type": "application/json",54 };55 const { action, invoice } = await req.json();5657 if (action === "get-invoices") {58 const res = await fetch(`${XERO_BASE}/Invoices?Statuses=AUTHORISED,SUBMITTED`, { headers });59 const data = await res.json();60 return new Response(JSON.stringify({ invoices: data.Invoices ?? [] }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });61 }62 if (action === "get-contacts") {63 const res = await fetch(`${XERO_BASE}/Contacts`, { headers });64 const data = await res.json();65 return new Response(JSON.stringify({ contacts: data.Contacts ?? [] }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });66 }67 if (action === "create-invoice" && invoice) {68 const res = await fetch(`${XERO_BASE}/Invoices`, { method: "POST", headers, body: JSON.stringify({ Invoices: [invoice] }) });69 const data = await res.json();70 return new Response(JSON.stringify({ invoice: data.Invoices?.[0] }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });71 }72 return new Response(JSON.stringify({ error: "Unknown action" }), { status: 400, headers: corsHeaders });73 } catch (err) {74 return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });75 }76});Pro tip: Xero access tokens expire after just 30 minutes — much shorter than most OAuth2 providers. The refresh logic in this Edge Function checks if the token expires within the next 60 seconds and proactively refreshes it. This prevents edge cases where a token expires between the check and the API call.
Expected result: The xero Edge Function is deployed. Calling it with a valid user JWT and action='get-invoices' returns invoices from the connected Xero organization. Token refresh happens automatically when needed.
Build the invoice management UI in Lovable
Build the invoice management UI in Lovable
With the Edge Function handling authentication and API proxying, build the invoice management frontend. Ask Lovable to create a dashboard that fetches and displays invoices from Xero, shows their status (DRAFT, SUBMITTED, AUTHORISED, PAID, VOIDED), and provides actions for common operations. Xero invoice data is rich: each invoice includes the Contact name, invoice number, issue date, due date, subtotal, total tax, total amount due, amount paid, and line items. The InvoiceID field is the Xero-internal UUID you'll need for payment recording and status updates. Display a clean invoice list with quick filters for 'Outstanding', 'Overdue', and 'Paid' statuses. For invoice creation, you need to know your Xero Contact IDs. Build a contact selector that calls get-contacts and lets the user pick from a dropdown. Xero invoice line items require: Description (free text), Quantity, UnitAmount, and AccountCode (the Xero chart of accounts code, e.g. '200' for Sales). Use a default AccountCode that makes sense for your business and allow it to be changed in advanced settings. Tip: Xero provides online invoice payment links if you have Xero payment services enabled — the invoice data includes an Url field with a payment link you can display to clients directly in your portal.
Build an invoice dashboard at /invoices. On load call the xero Edge Function with action='get-invoices'. Display invoices in a table: Contact name, Invoice #, Issue Date, Due Date, Amount Due, Status. Add filter tabs for AUTHORISED (outstanding), overdue (DueDate past today), and all. Add a Create Invoice button that opens a form: contact selector (populated by action='get-contacts'), description, quantity, unit price, account code (default '200'), due date. On submit, call xero with action='create-invoice' with the correct Xero invoice payload structure. Show the new invoice in the list after creation.
Paste this in Lovable chat
Pro tip: Xero's date fields use a .NET JSON date format: /Date(1234567890000+0000)/. When displaying dates, parse this format by extracting the timestamp from the parentheses. Alternatively, use the DateString field that Xero also returns in ISO 8601 format.
Expected result: The invoice dashboard displays Xero invoices grouped by status. The contact dropdown is populated from Xero. Creating an invoice through the form creates it in Xero and it appears in both Lovable and the Xero web interface.
Handle multi-organization access with Xero tenant management
Handle multi-organization access with Xero tenant management
One of Xero's unique features is that a single user can be connected to multiple organizations (tenants). An accountant, for example, might manage 20 client organizations from one Xero login. Your app needs to handle this gracefully. When the OAuth2 flow completes, call GET https://api.xero.com/connections to get the list of all tenants the user has authorized your app to access. Each connection has a tenantId, tenantType ('ORGANISATION'), and tenantName. Store all connections in Supabase and let the user select which organization to work with in your app's settings. Add an organization switcher to your navigation. When the user switches organizations, update the active tenant_id in their session or database record. All subsequent Edge Function calls will use the new tenant_id in the Xero-Tenant-Id header. For accountants building tools for multiple clients, you can display all organizations in a sidebar, allowing them to view invoices and contacts across multiple Xero organizations without needing to log in and out. Each organization's data is fetched with the same access token but a different Xero-Tenant-Id header.
Add a Xero organization switcher to my app. Create an Edge Function that calls GET https://api.xero.com/connections with the user's access token to get all connected organizations. Display a dropdown in the navigation showing all organizations with the current one selected. When the user switches, update their active_tenant_id in the xero_tokens table. All subsequent Xero API calls should use the active_tenant_id from that table.
Paste this in Lovable chat
Pro tip: If your app is for a single business owner (not an accountant managing multiple clients), you can skip the tenant switcher and simply use the first (and likely only) tenant in the connections list. The multi-tenant pattern is primarily valuable for accountants and bookkeepers with client access to multiple organizations.
Expected result: If the user has multiple Xero organizations, a dropdown appears showing all connected organizations. Switching organizations causes the invoice list to update with data from the selected organization.
Common use cases
Contractor invoice submission portal
A business that works with freelance contractors wants to give them a branded portal to submit hours worked, generate invoices automatically, and track payment status without needing access to the main Xero account. The Lovable app creates Xero invoices from submitted timesheet data and shows contractors real-time payment status pulled from Xero.
Build a contractor portal at /submit-invoice. Show a form with fields: hours_worked, hourly_rate, work_description, and period (week ending date). On submit, call an Edge Function that calculates the total, creates a Xero Invoice draft with the contractor as a Contact (looked up by email), sets due date to 30 days, and saves the Xero invoice ID to Supabase. Show the contractor a status page listing all their invoices with Xero payment status (DRAFT, SUBMITTED, AUTHORISED, PAID).
Copy this prompt to try it in Lovable
Client billing dashboard with outstanding balance view
An agency wants to show each client their outstanding invoices and payment history without giving clients full Xero access. The Edge Function fetches invoices filtered by the client's contact in Xero and displays them in a branded portal with amounts, due dates, and download links for invoice PDFs.
Create a client billing portal at /billing. When a client logs in, call an Edge Function to fetch all Xero invoices for their contact (identified by the email on their Supabase profile), returning invoice number, amount due, due date, and status. Display a table of outstanding invoices and a separate table of paid invoices from the last 12 months. Show total outstanding amount prominently at the top.
Copy this prompt to try it in Lovable
Automated bank reconciliation helper
An accountant wants a custom tool that pulls unreconciled bank transactions from Xero and suggests invoice matches based on amount and date proximity. The Edge Function fetches both unreconciled bank transactions and outstanding invoices, runs a matching algorithm, and presents suggestions the accountant approves before applying the reconciliation via Xero's API.
Build a bank reconciliation assistant. Create an Edge Function that fetches unreconciled bank transactions from Xero for the past 30 days and outstanding invoices of the same period. For each bank transaction, find invoices where the amount matches within 5% and the date is within 7 days. Display the suggested matches in a review interface where I can approve or reject each match. On approval, call the Xero API to create an allocation linking the payment to the invoice.
Copy this prompt to try it in Lovable
Troubleshooting
Xero API returns 403 AuthenticationUnsuccessful — Token has expired
Cause: The access token in xero_tokens has expired (Xero tokens last only 30 minutes) and the refresh logic failed or wasn't triggered. This can also happen if the refresh token has expired (60 days for production tokens) or was revoked by the user in Xero's app settings.
Solution: Check the expires_at column in your xero_tokens table and compare to the current time. If the token is expired, trigger a token refresh manually by calling the token endpoint with the refresh_token. If the refresh token is also expired, the user needs to re-authorize by going through the OAuth2 PKCE flow again. Add a check in your UI that detects 403 errors and prompts reconnection.
All API calls return 403 Forbidden — 'The AccessToken does not have the required scope'
Cause: The OAuth2 authorization was completed without the required scopes selected, or the scopes were changed in the app settings after the user authorized. The access token only grants the permissions the user approved during the OAuth2 flow — if accounting.transactions wasn't in the scope list, invoice calls will fail.
Solution: The user needs to re-authorize with the full scope list. Update XERO_SCOPES in Cloud Secrets to include all required scopes, then have the user click 'Reconnect Xero' to go through the authorization flow again. Xero will show a new consent screen with the updated permissions.
Invoice creation fails with 'Xero API error: validation exception' and Status=400
Cause: The invoice payload is missing required fields or uses invalid values. Common issues are: missing Contact.ContactID, invalid AccountCode that doesn't exist in the Xero chart of accounts, missing DueDate, or a Quantity of 0.
Solution: Check the Elements[].ValidationErrors array in Xero's error response — each validation error specifies the field and message. Fetch the chart of accounts from GET /Accounts to get valid AccountCode values for your organization. Ensure the ContactID is a valid UUID from a GET /Contacts call.
1// Minimal valid Xero invoice payload2const xeroInvoice = {3 Type: "ACCREC",4 Contact: { ContactID: "valid-uuid-from-xero" },5 DueDate: "/Date(1735689600000+0000)/",6 LineItems: [{7 Description: "Services",8 Quantity: 1.0,9 UnitAmount: 100.00,10 AccountCode: "200" // Must exist in chart of accounts11 }],12 Status: "DRAFT"13};GET /Connections returns empty array after OAuth2 authorization
Cause: The authorization completed but the user didn't select any Xero organizations to grant access to. On the Xero consent screen, users must check the checkbox next to each organization they want your app to access — if they proceed without selecting any, no connections are established.
Solution: Prompt the user to re-authorize. During the new authorization flow, instruct them explicitly to check at least one organization on the Xero consent screen. The GET /Connections call immediately after token exchange will confirm whether organizations were successfully linked.
Best practices
- Always refresh Xero access tokens proactively — check if the token expires within the next 60 seconds before making an API call, rather than waiting for a 401 response and retrying.
- Store the Xero-Tenant-Id per user in Supabase rather than hardcoding it in Cloud Secrets. This allows users with multiple Xero organizations to switch context and supports multi-tenant SaaS apps.
- Use Xero's modified-after filtering (If-Modified-Since header) for incremental data syncs — fetching only changed invoices since the last sync is far more efficient than fetching all invoices on every request.
- Never store Xero access tokens in Cloud Secrets — they change every 30 minutes and would constantly need updating. Store them in the xero_tokens table in Supabase with proper RLS protection instead.
- Handle Xero's .NET date format (/Date(timestamp+offset)/) in your parsing logic, or use the ISO date string alternatives (DateString, DueDateString) that Xero also provides in API responses.
- Test with the Xero Demo Company rather than a real organization during development — the Demo Company has pre-populated data covering all the data types your app will work with.
- Implement a 'Reconnect Xero' UI flow for when refresh tokens expire (every 60 days in production). Users should be able to re-authorize without losing their app data or settings.
Alternatives
QuickBooks dominates US and North American markets and uses standard OAuth2 without PKCE, making it slightly simpler to implement for US-focused apps.
Stripe has a native Lovable connector and handles invoicing and payment collection natively for apps that don't need full accounting integration.
Plaid provides bank account data that can supplement Xero when you need real-time bank balance information alongside accounting records.
Frequently asked questions
What's the difference between Xero's OAuth2 PKCE and QuickBooks OAuth2?
Both use the OAuth2 authorization code flow, but Xero requires PKCE (Proof Key for Code Exchange). PKCE adds a code_verifier (random string) and code_challenge (SHA-256 hash of the verifier) to the flow. The code_verifier is sent during token exchange instead of a client secret — this prevents authorization code interception attacks. In practice, the implementation difference is adding two extra parameters to the authorization URL and token exchange call.
How long do Xero access tokens and refresh tokens last?
Xero access tokens expire after 30 minutes — much shorter than most OAuth2 providers. Refresh tokens last 60 days from their last use (rolling expiry). If a refresh token hasn't been used in 60 days (for example, if a user doesn't log in for two months), it expires and the user must re-authorize. Plan for this in your app by adding a 'Reconnect Xero' flow.
Can one Lovable app connect to multiple Xero organizations?
Yes. A single OAuth2 authorization can grant access to multiple Xero organizations if the user selects multiple organizations on the Xero consent screen. Each connection is listed separately when you call GET /connections. Your app can store all tenant IDs and let users switch between organizations via a selector in the UI — all using the same access/refresh token pair.
Does Xero have a free development environment?
Yes. Xero provides a Demo Company — a pre-populated Xero organization with sample invoices, contacts, bank accounts, and transactions. You access it from your Xero account (look for the Demo Company option in the organization switcher). Your Xero Developer app connects to this Demo Company during development without touching any real accounting data.
How do I handle Xero API rate limits in Lovable?
Xero limits API calls to 60 requests per minute per app per organization, and 5,000 per day per organization. For a dashboard that multiple users access simultaneously, each hitting the same Xero organization, this can become a constraint. Cache Xero data in Supabase and implement a polling interval (e.g., refresh every 5 minutes) rather than fetching on every page load. For real-time needs, Xero's webhook subscriptions notify your app of data changes rather than requiring constant polling.
Does Xero support webhooks for real-time data sync?
Yes. Xero webhooks send POST notifications to your endpoint when accounting data changes (invoices created, payments received, contacts modified). Create a webhook receiver Edge Function, register its URL in your Xero app settings, and select the event types to subscribe to. Xero signs webhook payloads with HMAC-SHA256 using your webhook key — verify the signature in your Edge Function before processing events.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation