> ## Documentation Index
> Fetch the complete documentation index at: https://developers.pleo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Handle a High-Volume Chart of Accounts

> How to filter an Accounting System's chart of accounts, check it against Pleo's active Account limit, and sync it without exceeding that limit.

export const HighVolumeRejectionDiagram = () => {
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const check = () => setIsDark(document.documentElement.classList.contains("dark"));
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const nodeFill = isDark ? "#212222" : "#EEF4F4";
  const nodeStroke = isDark ? "#848989" : "#E1E6E6";
  const nodeTextStyle = isDark ? ",color:#EEF4F4" : ",color:#131414";
  const linkStyle = isDark ? "" : "linkStyle default stroke:#848989,stroke-width:1px;";
  const diagram = `
%%{init: {"themeVariables": {"fontSize": "28px"}}}%%
flowchart LR
    A[Batches sent without<br>the step 2 pre-check] --> B[Active count crosses<br>11,000 partway through]
    B --> C{Pleo Response?}
    C -->|HTTP 400| D[Log the rejection]
    D --> F[Abort sync]
    C -->|"201 with failed items"| E[Log each failed item's<br>real reasons]
    E --> G[Continue to next batch]

    style A white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style B white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style C white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style D white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style E white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style F white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style G white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
${linkStyle}
`;
  return <Mermaid chart={diagram} />;
};

export const HighVolumeCreateUnarchiveDiagram = () => {
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const check = () => setIsDark(document.documentElement.classList.contains("dark"));
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const nodeFill = isDark ? "#212222" : "#EEF4F4";
  const nodeStroke = isDark ? "#848989" : "#E1E6E6";
  const nodeTextStyle = isDark ? ",color:#EEF4F4" : ",color:#131414";
  const linkStyle = isDark ? "" : "linkStyle default stroke:#848989,stroke-width:1px;";
  const diagram = `
%%{init: {"themeVariables": {"fontSize": "12px"}}}%%
flowchart LR
    A[Account in filtered_as_accounts] --> B{State in Pleo?}
    B -->|Missing| C[Create]
    B -->|Archived| D[Unarchive]
    B -->|Already active| E[No action]

    style A white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style B white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style C white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style D white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style E white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
${linkStyle}
`;
  return <Mermaid chart={diagram} />;
};

export const HighVolumeArchiveDiagram = () => {
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const check = () => setIsDark(document.documentElement.classList.contains("dark"));
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const nodeFill = isDark ? "#212222" : "#EEF4F4";
  const nodeStroke = isDark ? "#848989" : "#E1E6E6";
  const nodeTextStyle = isDark ? ",color:#EEF4F4" : ",color:#131414";
  const linkStyle = isDark ? "" : "linkStyle default stroke:#848989,stroke-width:1px;";
  const diagram = `
%%{init: {"themeVariables": {"fontSize": "12px"}}}%%
flowchart LR
    A[Active Pleo Account] --> B{In filtered_as_accounts?}
    B -->|No| C[Archive]
    B -->|Yes| D[Leave active]

    style A white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style B white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style C white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style D white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
${linkStyle}
`;
  return <Mermaid chart={diagram} />;
};

export const HighVolumeCheckLimitDiagram = () => {
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const check = () => setIsDark(document.documentElement.classList.contains("dark"));
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const nodeFill = isDark ? "#212222" : "#EEF4F4";
  const nodeStroke = isDark ? "#848989" : "#E1E6E6";
  const nodeTextStyle = isDark ? ",color:#EEF4F4" : ",color:#131414";
  const linkStyle = isDark ? "" : "linkStyle default stroke:#848989,stroke-width:1px;";
  const diagram = `
%%{init: {"themeVariables": {"fontSize": "12px"}}}%%
flowchart LR
    A[filtered_as_accounts] --> B{11,000 or fewer?}
    B -->|Yes| C[Continue to step 3]
    B -->|No| D[Log error, abort sync]

    style A white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style B white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style C white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style D white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
${linkStyle}
`;
  return <Mermaid chart={diagram} />;
};

export const HighVolumeFilterDiagram = () => {
  const [isDark, setIsDark] = useState(false);
  useEffect(() => {
    const check = () => setIsDark(document.documentElement.classList.contains("dark"));
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const nodeFill = isDark ? "#212222" : "#EEF4F4";
  const nodeStroke = isDark ? "#848989" : "#E1E6E6";
  const nodeTextStyle = isDark ? ",color:#EEF4F4" : ",color:#131414";
  const linkStyle = isDark ? "" : "linkStyle default stroke:#848989,stroke-width:1px;";
  const diagram = `
%%{init: {"themeVariables": {"fontSize": "12px"}}}%%
flowchart LR
    A[All active AS Accounts] --> B[Filter: expense-relevant<br>types only] --> C[filtered_as_accounts]

    style A white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style B white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
    style C white-space:normal,fill:${nodeFill},stroke:${nodeStroke}${nodeTextStyle}
${linkStyle}
`;
  return <Mermaid chart={diagram} />;
};

export const NoteCallout = ({title, children}) => <div className="callout-box callout-note">
    <div className="callout-row">
      <span className="callout-icon">
        <svg width="22" height="22" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M208,104a79.86,79.86,0,0,1-30.59,62.92A24.29,24.29,0,0,0,168,186v6a8,8,0,0,1-8,8H96a8,8,0,0,1-8-8v-6a24.11,24.11,0,0,0-9.3-19A79.87,79.87,0,0,1,48,104.45C47.76,61.09,82.72,25,126.07,24A80,80,0,0,1,208,104Z" opacity="0.2" /><path d="M176,232a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h80A8,8,0,0,1,176,232Zm40-128a87.55,87.55,0,0,1-33.64,69.21A16.24,16.24,0,0,0,176,186v6a16,16,0,0,1-16,16H96a16,16,0,0,1-16-16v-6a16,16,0,0,0-6.23-12.66A87.59,87.59,0,0,1,40,104.49C39.74,56.83,78.26,17.14,125.88,16A88,88,0,0,1,216,104Zm-16,0a72,72,0,0,0-73.74-72c-39,.92-70.47,33.39-70.26,72.39a71.65,71.65,0,0,0,27.64,56.3A32,32,0,0,1,96,186v6h64v-6a32.15,32.15,0,0,1,12.47-25.35A71.65,71.65,0,0,0,200,104Zm-16.11-9.34a57.6,57.6,0,0,0-46.56-46.55,8,8,0,0,0-2.66,15.78c16.57,2.79,30.63,16.85,33.44,33.45A8,8,0,0,0,176,104a9,9,0,0,0,1.35-.11A8,8,0,0,0,183.89,94.66Z" /></svg>
      </span>
      <div>
        {title && <div className="callout-title">
            {title}
          </div>}
        <div className="callout-body">
          {children}
        </div>
      </div>
    </div>
  </div>;

export const PseudocodeNamingConventionNote = () => <NoteCallout title="Note">
    In the pseudocode on this page, <code>camelCase</code> names (for example <code>externalId</code>, <code>companyId</code>, <code>code</code>, <code>name</code>, <code>archived</code>) are real Pleo API fields. <code>snake_case</code> names are this example's own variables, not fields you'd see in a request or response.
  </NoteCallout>;

Pleo caps a company at 11,000 active Accounts (Chart of Accounts entries). Some Accounting Systems maintain a full chart of accounts with tens of thousands of entries. Only a small fraction of these are ever relevant to Pleo, but syncing the full set can exceed the limit. This how-to explains how to handle that limit in your integration.

## When to Use This

This how-to is useful when your integration is at risk of syncing more than 11,000 active Accounts to Pleo.

<NoteCallout title="Note">
  If the AS chart of accounts is small enough that this isn't a risk, skip the extra filtering and counting. Follow [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts) and [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts) directly.
</NoteCallout>

## Prerequisites

Before you begin:

* You're familiar with [Account Limits and High-Volume Sync Guidance](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-limits).
* Your integration is authenticated using one of the [supported authentication methods](/docs/current/integration-design/auth/integration-design-auth-overview#authentication-policy-overview).
* Your integration can call Pleo's Chart of Accounts API endpoints.

## Steps

<PseudocodeNamingConventionNote />

### 1. Filter to Expense-Relevant Accounts

<HighVolumeFilterDiagram />

Before fetching or matching anything, filter the AS chart of accounts to the accounts relevant for expense categorisation in Pleo. This is typically expense-type and P\&L (profit and loss) accounts. Asset, liability, equity, and bank accounts are generally not needed, so exclude them.

This step runs entirely on data already available from the AS. It does not call the Pleo API.

**Example Pseudo:**

```pseudo theme={null}
EXPENSE_RELEVANT_TYPES = {"EXPENSE", "PROFIT_AND_LOSS"}

all_as_accounts = fetchActiveAccountsFromAS()

filtered_as_accounts = []
for account in all_as_accounts:
    if account.type in EXPENSE_RELEVANT_TYPES:
        filtered_as_accounts.append(account)
```

The exact field used to classify accounts depends on what the AS exposes. If the AS does not expose an account type, fall back to the AS's own chart of accounts numbering convention (for example, a defined range of account codes for expense accounts).

### 2. Check the Filtered Count Against the Limit

<HighVolumeCheckLimitDiagram />

`filtered_as_accounts`, built in step 1, is what you want Pleo to have as active Accounts once the sync finishes. Check its size against the 11,000 limit before making any Pleo API calls.

**Example Pseudo:**

```pseudo theme={null}
filtered_count = len(filtered_as_accounts)

if filtered_count > 11000:
    log.error(
        f"Company {companyId}: filtered chart of accounts has "
        f"{filtered_count} accounts, exceeding Pleo's 11,000 "
        f"active Account limit by {filtered_count - 11000}. Sync aborted."
    )
    abortSync()
else:
    continueSync()  # proceed to step 3
```

You can do this check using only `filtered_as_accounts`, without looking at Pleo's current Accounts at all, because Chart of Accounts Sync is a full reconciliation. By the end of a run, Pleo's active Accounts always match `filtered_as_accounts` exactly:

* An account in `filtered_as_accounts` that's already active in Pleo: no change.
* An account in `filtered_as_accounts` that's missing, or archived, in Pleo: created or unarchived.
* An account active in Pleo but not in `filtered_as_accounts`: archived.

For example: Pleo has 10,000 active Accounts today, and `filtered_as_accounts` has 9,500:

| Outcome | Count |
| - | - |
| Already active in Pleo (no change needed) | 8,500 |
| Missing or archived in Pleo (create the missing ones, unarchive the rest) | 1,000 |
| Active in Pleo, but not in `filtered_as_accounts` (archive them) | 1,500 |

Archive the 1,500 first, then create or unarchive the 1,000. After the sync, Pleo has 9,500 active Accounts (8,500 + 1,000), matching `filtered_as_accounts` exactly.

### 3. Archive Accounts No Longer in the Filtered Set First

<HighVolumeArchiveDiagram />

Archive Accounts in this step. Account creation and unarchiving happen in the next step. Pleo never lets a company go over 11,000 active Accounts, and it checks the limit on both create and unarchive requests. If the company is close to the limit, a create or unarchive request sent before the archive step can be rejected with an `HTTP 400`, even though the sync would end up within the limit.

Fetch Pleo's current Chart of Accounts, including archived Accounts, the same way as [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts#2-retrieve-all-accounts-from-pleo), then follow [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts#3-archive-accounts-with-no-matching-as-entry) for the archive API request itself. The difference from that how-to is that you match against `filtered_as_accounts`, not `all_as_accounts` (the unfiltered list from step 1).

Accounts excluded by filtering, for example asset or liability accounts, get archived here if they were previously synced as active. This step catches them on every sync run, so they don't stay active in Pleo after they drop out of the filtered set.

**Example Pseudo:**

```pseudo theme={null}
pleo_accounts = fetchAccountsFromPleo(includeArchived: true)

filtered_external_ids = set()
for account in filtered_as_accounts:
    filtered_external_ids.add(account.externalId)

for account in pleo_accounts where archived == false:
    if account.externalId not in filtered_external_ids:
        account.archived = true
        PUT account to Pleo
```

### 4. Create or Unarchive the Remaining Accounts

<HighVolumeCreateUnarchiveDiagram />

Once the archive step has run, create the Accounts in `filtered_as_accounts` that are missing in Pleo, and unarchive the ones that were already archived in Pleo but that the sync now needs active.

* If you have many Accounts to create, use the batch endpoint instead of individual requests: follow [How to Create Multiple Accounts in a Single Request](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts) (max 1,000 Accounts per request).
* Unarchive existing Accounts individually: follow [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts).

### 5. Handle a Rejection from Pleo

<HighVolumeRejectionDiagram />

If a sync skips the step 2 pre-check, Pleo doesn't reject the whole sync at once. No single request can contain more than 1,000 Accounts (see [How to Create Multiple Accounts in a Single Request](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts)). Instead, once the company's active Accounts reach 11,000, further create calls (sent individually or as part of a batch) and further unarchive calls (always sent individually) get rejected outright with a top-level `HTTP 400`:

```json theme={null}
{"error": {"type": "BAD_REQUEST", "message": "Too many active accounts for company {companyId}"}}
```

A batch can still fail per-item for unrelated reasons, for example `EXTERNAL_ID_ALREADY_EXISTS` (see [Handling Failures](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts#handling-failures)), even when it isn't hitting the active Account limit. Check both error shapes in your response handling: a top-level `400` for exceeding the limit, and the per-item `failed` array for everything else.

**Example Pseudo:**

```pseudo theme={null}
for index, batch in enumerate(chunks(filtered_as_accounts, size=1000)):
    response = POST /v1/chart-of-accounts/batch with { companyId, items: batch }

    if response.status == 400:
        # Whole batch rejected, for example for exceeding the active Account limit
        log.error(f"Company {companyId}: batch {index}, containing {len(batch)} accounts, was rejected by Pleo: {response.body}")
        abortSync()
    else:
        # 201: the batch was accepted, but some items may have failed individually
        for failed_item in response.data.failed:
            log.error(f"Company {companyId}: account {failed_item.request.externalId} failed with reasons {failed_item.reasons}")
```

The same `400` can happen on a single unarchive call too, since unarchive is always sent individually via [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts). Check `response.status` on that call the same way.

***

## Error Codes

| Scenario | HTTP Status | Notes |
| - | - | - |
| A create or unarchive request would exceed the company's 11,000 active Account limit | `400` | `BAD_REQUEST`: "Too many active accounts for company `{companyId}`". See [5. Handle a Rejection from Pleo](#5-handle-a-rejection-from-pleo) above |
| One item's `externalId` already exists in Pleo | `201`, item in `data.failed[]` | See [How to Create Multiple Accounts in a Single Request: Error Codes](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts#error-codes) |
| The batch request is structurally invalid, for example more than 1,000 items | `400` | See [How to Create Multiple Accounts in a Single Request: Error Codes](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts#error-codes) |
| Too many requests in a short period | `429` | See [How to Create Multiple Accounts in a Single Request: Error Codes](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts#error-codes) |

***

## Related Reading

* [Account Limits and High-Volume Sync Guidance](/docs/current/integration-design/accounting-integrations/imports/accounts/integration-design-accounts-limits)
* [How to Sync Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-sync-accounts)
* [How to Create, Update, and Archive Accounts](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-create-update-archive-accounts)
* [How to Create Multiple Accounts in a Single Request](/docs/current/how-tos/accounting-integrations/imports/accounts/how-to-batch-create-accounts)

***
