# Multi-Company Support in Accounting Integrations

## Introduction

Many accounting platforms allow users to manage multiple companies, tenants, or entities within a single account. Apideck's Accounting API supports switching between these companies at request time using the `x-apideck-company-id` header, eliminating the need for separate connections per company.

## How It Works

When a consumer connects their accounting platform through Vault, they select a **default company**. This default is used for all API requests unless you explicitly override it using the `x-apideck-company-id` header.

### The x-apideck-company-id Header

Include this optional header in your Unified API requests to route the request to a specific company:

```bash
curl -X GET "https://unify.apideck.com/accounting/invoices" \
  -H "Authorization: Bearer {api-key}" \
  -H "x-apideck-app-id: {app-id}" \
  -H "x-apideck-consumer-id: {consumer-id}" \
  -H "x-apideck-service-id: xero" \
  -H "x-apideck-company-id: {company-id}"
```

When the header is:

- **Provided**: The request is routed to the specified company
- **Omitted**: The request uses the default company configured during connection setup

## Supported Connectors

The following accounting connectors support multi-company at request time:

| Connector                                                                             | Company Concept | Notes                                     |
| ------------------------------------------------------------------------------------- | --------------- | ----------------------------------------- |
| [Xero](/apis/accounting/xero)                                                         | Tenant          | One OAuth connection accesses all tenants |
| [Sage Business Cloud](/apis/accounting/sage-business-cloud-accounting)                | Business        | Multiple businesses per account           |
| [Sage Intacct](/apis/accounting/sage-intacct)                                         | Entity          | Multi-entity support within organization  |
| [Microsoft Dynamics 365 BC](/apis/accounting/microsoft-dynamics-365-business-central) | Company         | Multiple companies per environment        |
| [Exact Online](/apis/accounting/exact-online)                                         | Division        | Multiple divisions per account            |

>
> Connectors like QuickBooks Online, Zoho Books, and FreshBooks require one OAuth authorization per
> company. For these, you'll need to create separate connections for each company.

## Listing Available Companies

To retrieve the list of companies available for a connection, use the Companies endpoint:

```bash
curl -X GET "https://unify.apideck.com/accounting/companies" \
  -H "Authorization: Bearer {api-key}" \
  -H "x-apideck-app-id: {app-id}" \
  -H "x-apideck-consumer-id: {consumer-id}" \
  -H "x-apideck-service-id: xero"
```

**Response:**

```json
{
  "status_code": 200,
  "status": "OK",
  "data": [
    {
      "id": "c3d1b4e5-6789-4abc-def0-123456789abc",
      "name": "Acme Corp - US"
    },
    {
      "id": "f7e8d9c0-1234-5678-9abc-def012345678",
      "name": "Acme Corp - UK"
    }
  ]
}
```

Use the `id` value as the `x-apideck-company-id` header value in subsequent requests.

>
> The Companies endpoint is only available for connectors that support multi-company. Calling it on
> unsupported connectors will return an error.

## Identifying the Active Company via Metadata

Each connection exposes a standardized `metadata.company_id` field that reflects which organization the connection is currently authorized against. This value is automatically synced whenever the underlying company setting changes — whether through the Vault UI, the API, or after an OAuth authorization flow.

This field is populated for the multi-company connectors listed above **and** for single-company accounting connectors such as QuickBooks Online (`realm_id`) and NetSuite (`account_id`), where it identifies the one organization the connection is authorized against. In every case it gives you a connector-agnostic way to tell **which** organization a connection points at — and to detect when that changes.

### Why This Matters

Each connector uses a different internal setting for organization selection (e.g., Xero uses `tenant_id`, Sage Business Cloud uses `business_id`, QuickBooks uses `realm_id`, NetSuite uses `account_id`). Instead of having to know the connector-specific key, you can always read `metadata.company_id` for a consistent, connector-agnostic reference.

### Reading metadata.company_id

The field is available on the connection object returned by the [Vault API](/apis/vault/reference):

```bash
curl -X GET "https://unify.apideck.com/vault/connections/accounting/xero" \
  -H "Authorization: Bearer {api-key}" \
  -H "x-apideck-app-id: {app-id}" \
  -H "x-apideck-consumer-id: {consumer-id}"
```

The response includes the metadata:

```json
{
  "data": {
    "id": "accounting+xero",
    "service_id": "xero",
    "metadata": {
      "company_id": "c3d1b4e5-6789-4abc-def0-123456789abc"
    }
  }
}
```

### When It Syncs

`metadata.company_id` is updated automatically in these scenarios:

- **Settings update**: When a consumer changes the company selection via Vault or the API
- **OAuth authorization**: When a token-success hook auto-selects a company (e.g., Xero selecting the only available tenant)

>
> Existing connections have been backfilled with `metadata.company_id` wherever the underlying
> company setting was already captured. Connections that were missing that setting are populated on
> their next settings change or re-authorization. The backfill writes the field directly and does
> **not** emit `vault.connection.updated` webhooks, so it will not appear as an organization change
> to your webhook consumers.

### Connector Setting Key Mapping

For reference, here's which setting key maps to `metadata.company_id` for each connector:

| Connector                 | Setting Key        |
| ------------------------- | ------------------ |
| Xero                      | `tenant_id`        |
| Sage Business Cloud       | `business_id`      |
| Sage Intacct              | `entity`           |
| Microsoft Dynamics 365 BC | `company_id`       |
| Exact Online              | `systemDivisionId` |
| QuickBooks Online         | `realm_id`         |
| NetSuite                  | `account_id`       |

>
> QuickBooks Online and NetSuite are single-company-per-connection — they do not support
> request-time switching via the `x-apideck-company-id` header (see [Supported
> Connectors](#supported-connectors)). They still surface `metadata.company_id` so you can identify
> the authorized organization (QuickBooks realm, NetSuite account) and detect when a consumer
> re-authorizes a different one.

## Detecting Organization Changes

Because `metadata.company_id` is included on the connection object, and connection updates are
delivered via the `vault.connection.updated` webhook event, you can detect when a consumer
re-authorizes a connection against a **different** organization — without polling.

Subscribe to `vault.connection.updated` (see the [Webhooks Guide](/guides/webhooks)), then compare
`metadata.company_id` on the incoming payload against the value you last stored for that connection.
A change means the consumer switched or re-authorized against a different organization:

```typescript
// Handle an incoming connection.updated webhook event
const handleConnectionUpdated = async (event) => {
  const { id: connectionId, metadata } = event.payload
  const incomingCompanyId = metadata?.company_id

  const lastKnownCompanyId = await store.getCompanyId(connectionId)

  if (incomingCompanyId && incomingCompanyId !== lastKnownCompanyId) {
    // The authorized organization changed — resync or re-scope this connection's data
    await onOrganizationChanged(connectionId, lastKnownCompanyId, incomingCompanyId)
    await store.setCompanyId(connectionId, incomingCompanyId)
  }
}
```

>
> This is the recommended way to know **if** the authorized organization changed when you don't need
> the value itself — store the last-seen `metadata.company_id` per connection and diff it on each
> `vault.connection.updated` event. The one-time backfill does not emit `vault.connection.updated`,
> so the first change you observe reflects a genuine re-authorization.

## Common Use Cases

### 1. Multi-Tenant SaaS Applications

If your application serves users who manage multiple companies, you can let them switch between companies without re-authenticating:

```typescript
// Fetch invoices from a specific company
const getInvoices = async (consumerId: string, companyId: string) => {
  const response = await fetch('https://unify.apideck.com/accounting/invoices', {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'x-apideck-app-id': appId,
      'x-apideck-consumer-id': consumerId,
      'x-apideck-service-id': 'xero',
      'x-apideck-company-id': companyId
    }
  })
  return response.json()
}
```

### 2. Consolidated Reporting

Aggregate data across multiple companies by iterating through available companies:

```typescript
const getConsolidatedData = async (consumerId: string) => {
  // First, get all available companies
  const companiesResponse = await fetch('https://unify.apideck.com/accounting/companies', {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'x-apideck-app-id': appId,
      'x-apideck-consumer-id': consumerId,
      'x-apideck-service-id': 'xero'
    }
  })
  const { data: companies } = await companiesResponse.json()

  // Fetch invoices from each company
  const allInvoices = await Promise.all(
    companies.map(async (company) => {
      const invoices = await getInvoices(consumerId, company.id)
      return { companyId: company.id, companyName: company.name, invoices: invoices.data }
    })
  )

  return allInvoices
}
```

### 3. Company Selector in Your UI

Let users select which company to work with:

```typescript
// Populate a dropdown with available companies
const populateCompanySelector = async (consumerId: string) => {
  const response = await fetch('https://unify.apideck.com/accounting/companies', {
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'x-apideck-app-id': appId,
      'x-apideck-consumer-id': consumerId,
      'x-apideck-service-id': 'xero'
    }
  })
  const { data: companies } = await response.json()

  // companies array can be used to populate a <select> element
  return companies.map((c) => ({ value: c.id, label: c.name }))
}
```

## Virtual Webhooks and Multi-Company

[Virtual Webhooks](/guides/webhooks) currently only sync data from the **default company** configured during connection setup. The `x-apideck-company-id` header is not supported for webhook subscriptions.

If you need to receive webhook events from multiple companies, you have two options:

1. **Create separate connections**: Set up a connection per company, each with its own default company selection
2. **Use polling**: Query the API directly with the `x-apideck-company-id` header to fetch data from specific companies on demand

## Error Handling

### Invalid Company ID

The `x-apideck-company-id` value is passed directly to the downstream API without validation. If you provide an invalid or inaccessible company ID, the error response will come from the downstream connector and vary depending on the service.

For example, Xero might return:

```json
{
  "status_code": 403,
  "error": "Forbidden",
  "message": "AuthorizationUnsuccessful: The organisation is not authorised for this request"
}
```

Always use the [Companies endpoint](#listing-available-companies) to retrieve valid company IDs before making requests.

## Related Resources

- [Accounting API Reference](/apis/accounting/reference)
- [Vault Connection Guide](/guides/vault)
- [Webhooks Guide](/guides/webhooks)
- [Locations and Subsidiaries Guide](/guides/locations-subsidiaries-departments)
