Connect Xero with Shopify

Implementation Guide

Overview: Connecting Xero and Shopify

The Xero-to-Shopify integration addresses the accounting reconciliation gap that emerges as soon as an e-commerce business grows beyond the point where manual bookkeeping is sustainable. Shopify is an operational commerce platform: it processes orders, manages inventory, handles payments, and calculates tax. Xero is a double-entry accounting system: it manages the chart of accounts, reconciles bank transactions, handles payroll, and produces statutory financial statements. Neither platform is designed to replace the other, and the data flows between them — order data becoming invoices, payments reconciling against bank feeds, refunds generating credit notes — must be handled with accounting precision.

The most dangerous approach to this integration is treating it as a simple data copy. A Shopify order is not the same as a Xero invoice. A Shopify payment is not the same as a Xero bank transaction reconciliation. Incorrectly mapping these concepts creates double-counted revenue, unreconciled entries, and GST/VAT reporting errors that require a chartered accountant to untangle. This guide is written to prevent those errors by specifying the exact accounting logic that must govern each data flow.

This integration is available via Xero's native app marketplace (the A2X connector is the industry standard for high-volume Shopify-Xero reconciliation), via Zapier/Make for simpler use cases, or via direct REST API implementation for teams requiring custom accounting logic.

Core Prerequisites

For Xero API access, you must create a Xero App at developer.xero.com/app/manage. Select the "Web app" integration type for server-side OAuth 2.0 flows, or "Custom connection" for single-tenant integrations (recommended for internal tools connecting to a single Xero organisation). Required OAuth 2.0 scopes include accounting.transactions (read/write for invoices, credit notes, payments), accounting.contacts (read/write for customer records), accounting.settings.read (to retrieve tax rates, accounts, and currencies), and offline_access (to enable token refresh without user re-authentication). Xero access tokens expire after 30 minutes; refresh tokens expire after 60 days. Your implementation must handle token refresh automatically before each API call by checking token expiry against the stored timestamp.

You must also obtain the Xero Organisation's tenantId — a UUID that identifies the specific Xero organisation your app is connected to. After completing the OAuth flow, call GET https://api.xero.com/connections to retrieve the list of connected tenants and their IDs. This tenantId must be passed as the Xero-tenant-id header in every subsequent API request.

For Shopify API access, create a Custom App in your Shopify admin under Settings > Apps and sales channels > Develop apps. The required Admin API access scopes are read_orders, write_orders, read_customers, write_customers, read_products, and read_inventory. Generate an Admin API access token — this is a static token (not OAuth) that does not expire unless regenerated. Store it securely in your secret manager. For webhook-based real-time triggers, configure webhooks under Settings > Notifications in Shopify Admin, or programmatically via the Webhook API: POST https://{shop}.myshopify.com/admin/api/2024-04/webhooks.json.

Your Xero chart of accounts must be configured before the integration runs. You need: a dedicated Sales account for Shopify revenue (e.g., account code 200), a Shopify Clearing or Suspense account for payment processor settlements, a separate account for Shopify Fees/Payment Processing, and correct tax rates mapped to your Shopify tax settings. Mismatched tax rate codes between Shopify and Xero are the leading cause of GST/VAT reconciliation failures.

Top Enterprise Use Cases

The primary use case is automated invoice creation from Shopify orders. Each Shopify order, upon fulfillment or payment (depending on your revenue recognition policy), should create a corresponding Xero invoice. For B2C Shopify stores with high order volume, individual invoices per order are impractical and create Xero performance issues above ~2,000 invoices/month. The A2X approach — and the approach recommended here — is to batch orders into a single daily summary invoice that posts to Xero once per day, with line items broken down by product type, tax rate, and payment method. This is the accounting treatment preferred by most chartered accountants for retail e-commerce.

For B2B Shopify stores where individual invoice traceability matters, per-order invoicing is appropriate. In this model, each Shopify order generates a Xero invoice with the customer mapped to a Xero Contact, enabling statement-of-account and debtor aging features in Xero.

A second use case is refund and credit note reconciliation. Shopify refunds do not automatically reconcile in Xero. Each Shopify refund must create a Xero Credit Note against the original invoice, with the same line item breakdown and tax logic, and the credit note must be applied against the original invoice to zero it out.

A third use case is customer record synchronisation for B2B accounts. Large Shopify B2B customers should exist as Contacts in Xero with credit terms configured, so that Shopify orders generate Xero invoices with the correct payment terms (net 30, net 60) rather than defaulting to immediate payment.

Step-by-Step Implementation Guide

Begin with the Shopify webhook configuration. Register a webhook for the orders/paid topic to trigger invoice creation immediately upon payment. The registration request: POST https://{shop}.myshopify.com/admin/api/2024-04/webhooks.json Authorization: Bearer {SHOPIFY_ACCESS_TOKEN} Content-Type: application/json { "webhook": { "topic": "orders/paid", "address": "https://your-integration-service.com/webhooks/shopify/order-paid", "format": "json" } }

Shopify will POST the full order object to your endpoint. Verify the webhook's authenticity by computing HMAC-SHA256(raw_body, SHOPIFY_WEBHOOK_SECRET), base64-encoding the result, and comparing it to the X-Shopify-Hmac-Sha256 header. Reject requests that fail this check with a 401.

The Shopify order payload contains everything needed to construct a Xero invoice. A typical order object includes:

{
  "id": 5678901234,
  "name": "#1042",
  "email": "[email protected]",
  "total_price": "149.99",
  "subtotal_price": "136.35",
  "total_tax": "13.64",
  "currency": "GBP",
  "line_items": [
    {
      "title": "Enterprise Plan - Annual",
      "quantity": 1,
      "price": "136.35",
      "tax_lines": [{ "title": "VAT", "rate": 0.1, "price": "13.64" }]
    }
  ],
  "customer": { "email": "[email protected]", "first_name": "James", "last_name": "Parker" }
}

Before creating the Xero invoice, look up or create the Xero Contact for this customer. Query Xero contacts by email: GET https://api.xero.com/api.xro/2.0/Contacts?where=EmailAddress="[email protected]" with the Xero-tenant-id header set. If no contact is found, create one:

{
  "Name": "James Parker",
  "EmailAddress": "[email protected]",
  "FirstName": "James",
  "LastName": "Parker"
}

Note that Xero Contact Name must be unique within an organisation. For consumer e-commerce where many customers share common names, append the email address or Shopify customer ID to ensure uniqueness: "Name": "James Parker ([email protected])".

Construct the Xero invoice payload:

{
  "Type": "ACCREC",
  "Contact": { "ContactID": "{{xero_contact_id}}" },
  "InvoiceNumber": "SH-1042",
  "Reference": "Shopify Order #1042",
  "Date": "2025-06-15",
  "DueDate": "2025-06-15",
  "Status": "AUTHORISED",
  "LineAmountTypes": "EXCLUSIVE",
  "CurrencyCode": "GBP",
  "LineItems": [
    {
      "Description": "Enterprise Plan - Annual",
      "Quantity": 1.0,
      "UnitAmount": 136.35,
      "AccountCode": "200",
      "TaxType": "OUTPUT2"
    }
  ]
}

The TaxType field is critical: it must match exactly the Xero tax rate code configured in your organisation. OUTPUT2 is the standard UK 20% VAT rate. Passing an incorrect tax type results in a 400 error with TaxRateNotFound. Retrieve your organisation's valid tax rates from GET https://api.xero.com/api.xro/2.0/TaxRates before hardcoding any tax type value.

After creating the invoice with POST https://api.xero.com/api.xro/2.0/Invoices, immediately create a Payment against it to mark it as paid (since Shopify only sends the orders/paid webhook after payment is confirmed):

{
  "Invoice": { "InvoiceID": "{{new_invoice_id}}" },
  "Account": { "Code": "070" },
  "Date": "2025-06-15",
  "Amount": 149.99
}

Account code 070 should be your Shopify Clearing account. This two-step pattern (create invoice, then apply payment) ensures Xero's double-entry ledger remains balanced and the invoice does not sit as an open receivable.

For refunds, register a webhook on refunds/create. When received, query Xero for the original invoice by the InvoiceNumber (e.g., SH-1042), then create a Credit Note:

{
  "Type": "ACCREC",
  "Contact": { "ContactID": "{{contact_id}}" },
  "Date": "2025-06-16",
  "Status": "AUTHORISED",
  "LineItems": [
    {
      "Description": "Refund for Order #1042",
      "Quantity": 1.0,
      "UnitAmount": 149.99,
      "AccountCode": "200",
      "TaxType": "OUTPUT2"
    }
  ]
}

Then allocate the credit note against the original invoice using the Allocations endpoint to complete the accounting entry.

Common Pitfalls & Troubleshooting

A 400 Bad Request from the Xero API with error ContactNotFound during invoice creation means the Contact lookup returned no results and your code attempted to create an invoice referencing a ContactID that does not exist. Always assert that the Contact creation or lookup step returned a valid UUID before proceeding to invoice creation.

A 403 Forbidden with AuthenticationUnsuccessful most commonly occurs when the Xero-tenant-id header is missing from the request or contains an incorrect value. Confirm the tenant ID by calling GET https://api.xero.com/connections and verifying the UUID against your stored value.

Xero imposes a rate limit of 60 API calls per minute per app per organisation, and a daily limit of 5,000 calls. For high-volume Shopify stores processing hundreds of orders per hour during peak periods (Black Friday, product launches), this rate limit will be hit. Implement a queue-based architecture where incoming Shopify webhooks are written to a message queue (SQS, Redis, or a database table), and a worker process consumes them at a controlled rate of no more than 1 API call per second. When a 429 Too Many Requests is received, back off for 60 seconds before retrying.

The most significant long-term operational risk is tax rate drift: if your Shopify tax settings change (adding a new tax region, modifying rates) without corresponding updates to the Xero tax type mapping in your integration, invoices will be created with incorrect VAT/GST values. Implement an alert that fires whenever a new tax_lines title appears in an incoming Shopify payload that is not present in your configured mapping table, so your team can update the mapping before the first affected invoice is created.