# Balance sheet syncing via the Custom API integration

The Custom API accountancy integration supports **balance sheet syncing**, just like Breww's built-in integrations with Xero and QuickBooks Online. This lets you keep your balance sheet and COGS in your accountancy platform up-to-date as costs move through Breww from purchase to production to sale.

If you're not yet familiar with how balance sheet syncing works in Breww, read [Balance sheet syncing](https://breww.com/docs/balance-sheet-syncing/) first. The concepts (Breww-controlled accounts, the initial offsetting journal entry, drink-type overrides, etc.) all apply to the Custom API integration in the same way.

This page covers what's different when you're using the Custom API integration: the developer-facing endpoints your sync code calls to fetch journal entries and confirm them back to Breww.

## How it differs from a built-in integration

With Breww's built-in Xero and QuickBooks Online integrations, Breww posts journal entries directly to your accountancy platform on a schedule. With the Custom API integration, your own sync code is responsible for that step, and the flow is **client-driven**:

1. Your code calls a `GET` endpoint. At the moment of that call, Breww builds the set of journal entries that need to be synced and returns them, together with a `confirmation_id`.
2. Your code formats those entries for your accountancy platform and uploads them.
3. Your code calls a `POST /confirm/` endpoint, echoing the `confirmation_id` back, to tell Breww whether the upload succeeded or failed.

   There is **no scheduled sync** for the Custom integration, and **no realtime push** when individual invoices or inventory receipts are finalised. Nothing is built until you call `GET`. This is intentional — the middleware owns the cadence.

## Enabling balance sheet syncing

1. Open your Custom API accountancy integration in **Integrations** → **Accountancy**.
2. Make sure your chart of accounts is set up — see [Chart of accounts (nominal codes)](https://breww.com/docs/nominal-codes-accountancy-accounts/). The full list of areas you need accounts for is in [Balance sheet syncing](https://breww.com/docs/balance-sheet-syncing/#enabling-balance-sheet-syncing).
3. From the integration detail page, find the **Balance sheet syncing** row in the **Data syncing** section and click **Enable**.
4. Step through the setup wizard. The final step asks you to pick an **offsetting account** (typically a dedicated suspense/clearing account) — this is where Breww's one-off opening adjustment will be posted, so your Breww-controlled accounts start with the right balances.

   Once enabled, the `GET` endpoint will start returning journal entries the first time you call it.

> **Note:**
> Any customer or supplier touched by a sync cycle (for example a supplier on an inventory receipt that's being posted to GRNI) must already have its `accountancy_provider_id` set in Breww before the first sync attempt. The `GET` endpoint will return `422` with a list of unmapped entities if it finds any. See ["Required default accounts and entity mapping"](#required-default-accounts-and-entity-mapping) below.

## The endpoints

All requests follow the same authentication scheme as the rest of the Custom API integration. You'll need your integration's `auth_id` and an API token — see [Custom API accountancy integration](https://breww.com/docs/custom-api-accountancy-integration/) for details.

### Fetching pending journal entries

```
GET https://breww.com/api/accountancy-sync-balance-sheet-journal-entries/<auth_id>/
```

At the moment you call this endpoint, Breww calculates which records in your account have drifted since their last successful sync, builds the journal entries needed to bring your accountancy platform back in line, and returns them.

A successful response looks like:

```json
{
  "confirmation_id": "019e20e8-c74e-757e-bc5d-4dc3f7d10b33",
  "journal_entries": [
    {
      "id": null,
      "date": "2026-05-13",
      "reference": "Breww: Invoice 26",
      "currency": "GBP",
      "lines": [
        {
          "description": "Cost of goods sold for Invoice 26",
          "amount": "12.34",
          "account": {
            "id": "1003",
            "nominal_code": "5000",
            "name": "Cost of goods sold",
            "type": "Expense",
            "status": "Active",
            "is_bank_account": false,
            "currency": "GBP"
          },
          "entity": null
        }
      ]
    }
  ]
}
```

Notable fields:

* **`confirmation_id`** — a UUID identifying this specific batch of entries. You **must** echo it back when you confirm, otherwise the confirm will be rejected (see ["The](#the-confirmation_id-contract) [`confirmation_id`](#the-confirmation_id-contract) [contract"](#the-confirmation_id-contract)).

* **`journal_entries[].id`** — always `null` for the Custom integration. Breww has no journal-entry ID to give you because it doesn't know what ID your accountancy platform will assign. See ["Updates are sent as adjustments, not edits"](#updates-are-sent-as-adjustments-not-edits).

* **`journal_entries[].lines[].amount`** — a **signed** decimal in the account's natural balance direction. See the next section — this is the most important piece for a middleware author to get right.

* **`journal_entries[].lines[].entity`** — an object identifying a customer or supplier (with `provider_id`, `name`, `reference`, `type`) when the line relates to one. May be `null`.

* **`journal_entries[].lines[].account.type`** — one of `Asset`, `Liability`, `Equity`, `Income`, `Expense`. Drives how `amount` is translated into debit/credit (see below).

  The response body has no per-row `uploaded_to_accountancy_provider` status flag — the other Custom-API accountancy-sync endpoints use that filter, but balance sheet syncing works in **confirmation rounds**, not row-by-row status.

### Confirming or discarding a batch

```
POST https://breww.com/api/accountancy-sync-balance-sheet-journal-entries/<auth_id>/confirm/
```

```json
{
  "confirmation_id": "019e20e8-c74e-757e-bc5d-4dc3f7d10b33",
  "success": true
}
```

Send `success: true` once you've successfully uploaded **every** entry from the batch to your accountancy platform. Breww will commit the deferred `*_last_synced_*` field updates so those records won't be re-included in the next `GET`.

Send `success: false` if your upload failed for any reason. Breww will **discard** the pending batch without committing the field updates. The next `GET` rebuilds a fresh batch (with a new `confirmation_id`) reflecting the latest state in Breww.

> **Note:**
> Always confirm or discard. If your middleware crashes mid-upload and never calls the confirm endpoint, the pending batch remains in place — subsequent `GET` calls return the exact same body and the same `confirmation_id` indefinitely (see ["The](#the-confirmation_id-contract) [`confirmation_id`](#the-confirmation_id-contract) [contract"](#the-confirmation_id-contract)). Confirming with `success: false` from your error-handling path keeps the cycle moving.

The body is a **single object** — not a list. There are no external IDs to post back, because Breww doesn't track which of your accountancy-side entries corresponds to which delivered entry.

## The signed-amount convention

This is the single most important detail for a middleware integration. Breww does **not** send traditional debit/credit pairs. Each line carries a **signed** **`amount`** in the natural balance direction of the line's `account.type`:

| Account type | `amount` > 0 means           | `amount` < 0 means          |
| ------------ | ---------------------------- | --------------------------- |
| `Asset`      | Debit (asset increases)      | Credit (asset decreases)    |
| `Expense`    | Debit (expense increases)    | Credit (expense decreases)  |
| `Liability`  | Credit (liability increases) | Debit (liability decreases) |
| `Equity`     | Credit (equity increases)    | Debit (equity decreases)    |
| `Income`     | Credit (income increases)    | Debit (income decreases)    |

A balanced **goods-received-not-invoiced** entry, for example, looks like this in the payload:

```
Accrued Liabilities  (Liability):  +919.80   <- liability up
Raw Materials        (Asset):      +919.80   <- asset up
```

Both lines have the same sign because both balances are moving up in their natural direction. Your middleware should translate each line into the debit/credit shape expected by your accountancy platform using the rules in the table above. Every entry Breww emits is internally balanced when translated this way.

## The `confirmation_id` contract

Each `GET` response includes a `confirmation_id`. Your subsequent `POST /confirm/` **must** echo the same `confirmation_id` back.

Specifically:

* While a pending batch exists, repeated `GET` calls return the **same** body and the **same** `confirmation_id`. They are idempotent.

* Once you `POST /confirm/` — with either `success: true` or `success: false` — the pending batch is consumed. The next `GET` builds a fresh batch with a **new** `confirmation_id`, reflecting any drift that has accumulated.

* Confirming with the wrong (or stale) `confirmation_id` returns `409 Conflict`. The remedy is to re-issue `GET`, take the new `confirmation_id` from the response, and confirm with that.

  This mechanism makes the GET/POST cycle safe against double-confirmations, retries that race against new sync cycles, and stale pending batches in the middleware.

## Updates are sent as adjustments, not edits

The Custom integration does **not** support updating previously-synced journal entries. If a brewery user changes a record that was already synced (for example by editing a batch's ingredient costs), Breww will not send an updated version of the original entry. Instead, the next sync cycle delivers a **new adjustment entry** containing only the delta.

That's why every entry's `id` field is `null` in the payload: there is no Breww-side journal-entry identity for your middleware to update against. Treat each delivered entry as **additive** — post it as a new journal entry on your accountancy side.

## Required default accounts and entity mapping

### Default account configuration

Before the `GET` endpoint will return anything, your integration's chart of accounts must have all of the following defaults set. The endpoint returns `400` with a list of any that are missing.

* `default_sales_account`

* `default_stock_item_account`

* `default_goods_in_additional_cost_account`

* `default_finished_goods_account`

* `default_cogs_account`

* `default_wip_account`

* `default_wip_losses_account`

* `default_stock_adjustment_account`

* `default_batch_other_cost_account`

* `default_cm_cost_account`

* `accrued_liabilities_account`

  These are set during the balance sheet syncing setup wizard. You can review and change them later on the integration's **Accounts & tracking** page.

### Per-supplier and per-customer mapping

Any supplier touched by an inventory receipt being synced, and any customer touched by a sale being synced, must already have its `accountancy_provider_id` set in Breww. If Breww finds an unmapped entity during the build, it accumulates a user-facing message and returns `422` with all of them in one response, so the brewery user (and your middleware) can see everything that needs fixing in a single round-trip.

Map customers and suppliers either through the Breww UI (`Map customers & suppliers` action on the integration detail page) or via the existing `accountancy-sync-customers` and `accountancy-sync-suppliers` endpoints described in [Custom API accountancy integration](https://breww.com/docs/custom-api-accountancy-integration/).

## Error responses

| Status | Meaning                                                                                                                                                                                                                                                                                                                                                                                      |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | On `GET`: the `journal_entries` array is included (it may be empty if there's been no drift). On `POST /confirm/`: the batch was confirmed or discarded successfully.                                                                                                                                                                                                                        |
| `400`  | One or more required default accounts are missing. The response body lists which.                                                                                                                                                                                                                                                                                                            |
| `404`  | On `GET`: the `auth_id` doesn't belong to a Custom accountancy integration for your brewery. On `POST /confirm/`: there's no pending batch to confirm.                                                                                                                                                                                                                                       |
| `409`  | The `confirmation_id` in the `POST` body doesn't match the current pending batch. Re-issue `GET` and use the new id.                                                                                                                                                                                                                                                                         |
| `422`  | User action is required before syncing can continue. The response body contains `errors` (a list of user-facing messages) and `suggested_solution`. Typical causes: the wizard hasn't been completed (`sync_activity_to_balance_sheet` is `false`), or a touched customer/supplier is missing its `accountancy_provider_id`. Surface these to the brewery user rather than retrying blindly. |

<br />

## Where this fits in your sync workflow

The balance-sheet endpoint is a separate step in your scheduled sync loop, alongside the customer payments, orders, credit notes, and supplier invoices endpoints described in [Custom API accountancy integration](https://breww.com/docs/custom-api-accountancy-integration/). The recommended order is:

1. Map customers and suppliers.
2. Upload customer payments.
3. Upload orders, credit notes, and supplier invoices.
4. Upload payment and credit note allocations.
5. **Fetch and confirm balance sheet journal entries.**

   Journal entries don't have ordering dependencies with the other document types — they reference accounts (via the codes you've configured in your chart of accounts), not invoices or payments. You can fit the `GET`/`POST confirm` round-trip in at any point in the loop.

## Troubleshooting

### The `GET` endpoint returns an empty `journal_entries` list

This is normal when nothing has drifted since the last successful confirmation. The cycle still completes — you'll still get a `confirmation_id`, and you should still `POST /confirm/` with `success: true` (with no entries to upload, this is essentially a no-op).

### Repeated `GET` calls return the same body forever

You haven't confirmed the pending batch. Call `POST /confirm/` with the `confirmation_id` from the response — either `success: true` if you've uploaded the entries, or `success: false` to discard the batch and pick up a fresh one on the next `GET`.
