Xero Adapter

The Xero adapter provides access to accounting data including contacts, invoices, and items with OAuth2 authentication and multi-tenant organization support.

Installation

npm install @openetl/xero

Features

  • OAuth2 Authentication - Full OAuth2 flow with automatic token refresh
  • Multi-Tenant Support - Work with multiple Xero organizations
  • OAuth Helpers - Built-in methods for authorization flow
  • Rate Limiting - Automatic handling of Xero API rate limits
  • Full CRUD Support - Download and create resources
  • Core Endpoints - Contacts, Invoices, Items, and Accounts

Authentication

The Xero adapter uses oauth2 authentication:

const vault = {
  'xero-auth': {
    id: 'xero-auth',
    type: 'oauth2',
    credentials: {
      client_id: process.env.XERO_CLIENT_ID,
      client_secret: process.env.XERO_CLIENT_SECRET,
      access_token: process.env.XERO_ACCESS_TOKEN,
      refresh_token: process.env.XERO_REFRESH_TOKEN,
    },
  },
};

OAuth2 Setup

Step 1: Generate Authorization URL

import { XeroAdapter } from '@openetl/xero';

const authUrl = XeroAdapter.helpers.getCode(
  'https://yourapp.com/callback',  // Your redirect URI
  process.env.XERO_CLIENT_ID
);

// Redirect user to authUrl
// User authorizes your app
// Xero redirects back with ?code=xxx

Step 2: Exchange Code for Tokens

const tokens = await XeroAdapter.helpers.getTokens(
  'https://yourapp.com/callback',
  process.env.XERO_CLIENT_ID,
  process.env.XERO_CLIENT_SECRET,
  { code: 'authorization_code_from_callback' }
);

// tokens = {
//   access_token: '...',
//   refresh_token: '...',
//   expires_in: 1800,
//   token_type: 'Bearer'
// }

Step 3: Get Tenant ID

After authentication, get connected organizations:

const response = await fetch('https://api.xero.com/connections', {
  headers: {
    'Authorization': `Bearer ${tokens.access_token}`,
    'Content-Type': 'application/json',
  },
});

const connections = await response.json();
// [{ tenantId: 'xxx', tenantName: 'My Company', ... }]

const tenantId = connections[0].tenantId;

Quick Start

import { Orchestrator } from 'openetl';
import { xero } from '@openetl/xero';

const vault = {
  'xero-auth': {
    id: 'xero-auth',
    type: 'oauth2',
    credentials: {
      client_id: process.env.XERO_CLIENT_ID,
      client_secret: process.env.XERO_CLIENT_SECRET,
      access_token: process.env.XERO_ACCESS_TOKEN,
      refresh_token: process.env.XERO_REFRESH_TOKEN,
    },
  },
};

const etl = Orchestrator(vault, { xero });

const pipeline = {
  id: 'export-contacts',
  source: {
    id: 'contacts-source',
    adapter_id: 'xero',
    endpoint_id: 'contacts',
    credential_id: 'xero-auth',
    config: {
      tenant_id: 'your-xero-tenant-id',
    },
    fields: ['ContactID', 'Name', 'EmailAddress', 'FirstName', 'LastName'],
    pagination: { type: 'page', itemsPerPage: 100 },
  },
};

const result = await etl.runPipeline(pipeline);
console.log(`Exported ${result.data.length} contacts`);

Endpoints

contacts

Retrieve or create contacts.

Config Property Required Description
tenant_id Yes Xero organization ID
const connector = {
  adapter_id: 'xero',
  endpoint_id: 'contacts',
  credential_id: 'xero-auth',
  config: {
    tenant_id: 'your-tenant-id',
  },
  fields: ['ContactID', 'Name', 'EmailAddress', 'FirstName', 'LastName', 'Phones'],
  pagination: { type: 'page', itemsPerPage: 100 },
};

Common contact fields:

  • ContactID - Unique identifier
  • Name - Contact/company name
  • EmailAddress - Primary email
  • FirstName, LastName - Contact person name
  • Phones - Phone numbers array
  • Addresses - Address array

invoices

Retrieve or create invoices.

const connector = {
  adapter_id: 'xero',
  endpoint_id: 'invoices',
  credential_id: 'xero-auth',
  config: {
    tenant_id: 'your-tenant-id',
  },
  fields: ['InvoiceID', 'InvoiceNumber', 'Contact', 'Total', 'Status', 'DueDate'],
  filters: [
    { field: 'Status', operator: '=', value: 'AUTHORISED' },
  ],
  pagination: { type: 'page', itemsPerPage: 100 },
};

Common invoice fields:

  • InvoiceID - Unique identifier
  • InvoiceNumber - Invoice number
  • Type - ACCREC (receivable) or ACCPAY (payable)
  • Contact - Associated contact
  • Total - Total amount
  • Status - DRAFT, SUBMITTED, AUTHORISED, PAID, VOIDED

Creating invoices:

const invoice = {
  Type: 'ACCREC',
  Contact: { ContactID: 'contact-uuid' },
  LineItems: [
    { Description: 'Consulting', Quantity: 10, UnitAmount: 150.00, AccountCode: '200' }
  ],
  DueDate: '2024-12-31',
  Status: 'DRAFT',
};

items

Retrieve or create inventory items.

const connector = {
  adapter_id: 'xero',
  endpoint_id: 'items',
  credential_id: 'xero-auth',
  config: { tenant_id: 'your-tenant-id' },
  fields: ['ItemID', 'Code', 'Name', 'Description', 'PurchaseDetails', 'SalesDetails'],
};

accounts

Retrieve chart of accounts.

const connector = {
  adapter_id: 'xero',
  endpoint_id: 'accounts',
  credential_id: 'xero-auth',
  config: { tenant_id: 'your-tenant-id' },
  fields: ['AccountID', 'Code', 'Name', 'Type', 'Status'],
};

Filtering

Xero supports limited filtering:

filters: [
  { field: 'Status', operator: '=', value: 'ACTIVE' },
  { field: 'ContactID', operator: '=', value: 'guid-here' },
]

Note: Not all fields support filtering. Consult the Xero API documentation for supported filter fields.

Complete Example

import { Orchestrator, Pipeline } from 'openetl';
import { xero } from '@openetl/xero';
import { postgresql } from '@openetl/postgresql';

const vault = {
  'xero-auth': {
    id: 'xero-auth',
    type: 'oauth2',
    credentials: {
      client_id: process.env.XERO_CLIENT_ID,
      client_secret: process.env.XERO_CLIENT_SECRET,
      access_token: process.env.XERO_ACCESS_TOKEN,
      refresh_token: process.env.XERO_REFRESH_TOKEN,
    },
  },
  'warehouse': {
    id: 'warehouse',
    type: 'basic',
    credentials: {
      host: 'localhost',
      database: 'analytics',
      username: 'etl',
      password: process.env.DB_PASSWORD,
    },
  },
};

const etl = Orchestrator(vault, { xero, postgresql });

const pipeline: Pipeline = {
  id: 'sync-invoices',
  source: {
    id: 'invoices-source',
    adapter_id: 'xero',
    endpoint_id: 'invoices',
    credential_id: 'xero-auth',
    config: {
      tenant_id: process.env.XERO_TENANT_ID,
    },
    fields: ['InvoiceID', 'InvoiceNumber', 'Total', 'Status', 'DueDate'],
    filters: [
      { field: 'Status', operator: '=', value: 'AUTHORISED' },
    ],
    pagination: { type: 'page', itemsPerPage: 100 },
  },
  target: {
    id: 'invoices-target',
    adapter_id: 'postgresql',
    endpoint_id: 'table_insert',
    credential_id: 'warehouse',
    config: { schema: 'accounting', table: 'xero_invoices' },
    fields: ['invoice_id', 'invoice_number', 'total', 'status', 'due_date'],
  },
  error_handling: {
    max_retries: 3,
    retry_interval: 2000,
    fail_on_error: true,
  },
};

const result = await etl.runPipeline(pipeline);
console.log(`Synced ${result.data.length} invoices`);

Rate Limiting

Xero has strict rate limits. The adapter handles rate limit responses (HTTP 429) with exponential backoff.

error_handling: {
  max_retries: 5,
  retry_interval: 2000,
  fail_on_error: true,
}

Resources

Related