# Spend and vendor analytics with the Accounting API

Spend management, procurement intelligence and finance analytics products need a clean, deduplicated view of who a company pays, what they pay for, and how they pay. The walkthrough below reads that data from a customer's ledger through Apideck's [Accounting API](/apis/accounting/reference) and turns it into supplier spend, category spend, period spend and vendor behavior metrics. It is a read-only use case: nothing in this guide writes back to the customer's accounting system.

## One integration for every ledger

A spend analytics product that integrates one accounting system at a time ships one connector per quarter. Built against Apideck, the same product covers the long tail with one integration, and the data model stays identical as new connectors come online.

- No connector-specific supplier, bill or payment schemas to normalize downstream.
- One pagination model and one `filter[...]` syntax across every ledger.
- Classes, cost centers and dimensions map to `tracking_categories`, with departments, locations and subsidiaries available as their own resources.
- New connectors appear on the same endpoints, so onboarding a customer on Pennylane or Rillet needs no new code.

## Resource mapping

The pivot entity for spend analytics is the supplier. The unified `suppliers` resource maps to the following downstream objects across the connectors that handle most real-world traffic for this use case:

| Connector | Downstream object |
| --- | --- |
| QuickBooks | `Vendor` |
| Xero | `Contact` with `IsSupplier=true` |
| NetSuite | `Vendor` |
| Sage Intacct | `Vendor` |
| Microsoft Dynamics 365 Business Central | `Vendor` |
| Exact Online | `Account` with supplier status |
| Odoo | `res.partner` with `supplier_rank > 0` |
| Pennylane | `Supplier` |
| Zoho Books | `Vendor` |
| Workday | `Supplier` |

The other resources used in this guide follow the same pattern: `bills` map to vendor bills or purchase invoices, `bill-payments` to supplier payments or bill payment transactions, `expenses` to purchases, bank spend money or expense reports, `purchase-orders` to purchase orders, `ledger-accounts` to the chart of accounts, and `tracking-categories` to classes, dimensions, cost centers or tracking categories depending on the connector.

## Walkthrough

The pipeline is a sequence of paginated reads followed by in-memory aggregation. Pull dimensions first, then the master data, then the transactions.

### 1. Pull dimensions: ledger accounts and tracking categories

Ledger accounts are the category axis of spend analytics. Pull them once per connection and cache them, keyed by `id`. Tracking categories give the department, project, cost center or class axis.

`LedgerAccountsFilter` has no `type` filter, and `filter[classification]` takes a single value. Pull the full chart of accounts and keep the spend accounts client-side, on `classification` values `expense`, `other_expense` and `costs_of_sales` or on `type` values `expense`, `other_expense` and `costs_of_sales`.

Send this to [`GET /accounting/ledger-accounts`](/apis/accounting/reference#operation/ledgerAccountsAll):

```http
GET /accounting/ledger-accounts?limit=200 HTTP/1.1
Host: unify.apideck.com
Authorization: Bearer {API_KEY}
x-apideck-app-id: {APP_ID}
x-apideck-consumer-id: {CONSUMER_ID}
x-apideck-service-id: quickbooks
```

Follow up with [`GET /accounting/tracking-categories`](/apis/accounting/reference#operation/trackingCategoriesAll) to pull the dimension values that will later join against bill and expense line items. This endpoint takes no filters, so pull it in full on every run.

### 2. Pull the supplier master

Supplier records anchor the dedup pass and hold the metadata the analytics UI renders (tax ID, address, default payment terms). Page through [`GET /accounting/suppliers`](/apis/accounting/reference#operation/suppliersAll) once for the full backfill, then pass `filter[updated_since]` on every subsequent run.

```http
GET /accounting/suppliers?limit=200&filter[updated_since]=2024-11-01T00:00:00Z HTTP/1.1
Host: unify.apideck.com
Authorization: Bearer {API_KEY}
x-apideck-app-id: {APP_ID}
x-apideck-consumer-id: {CONSUMER_ID}
x-apideck-service-id: quickbooks
```

A typical response row carries the fields spend analytics needs:

```json
{
  "id": "sup_01HQAZ7F3K8M2N5P7R9S1T3V5X",
  "display_name": "Northwind Office Supplies",
  "company_name": "Northwind Office Supplies Ltd",
  "tax_number": "GB345834921",
  "currency": "GBP",
  "payment_method": "bank_transfer",
  "addresses": [
    {
      "country": "GB",
      "city": "Manchester",
      "postal_code": "M1 4BT"
    }
  ],
  "updated_at": "2025-01-14T09:12:41Z"
}
```

### 3. Pull bills and expenses

Bills are the primary spend ledger for accounts payable, and expenses capture the paid-at-point-of-sale card and bank transactions that never pass through AP. Both carry supplier references, line items, line-level ledger accounts and, where supported, tracking categories.

Call [`GET /accounting/bills`](/apis/accounting/reference#operation/billsAll) with `filter[updated_since]` for incremental runs:

```json
{
  "id": "bill_01HQAZ8G4M9N3P6R8T0V2W4Y6Z",
  "bill_number": "NW-2025-00419",
  "supplier": {
    "id": "sup_01HQAZ7F3K8M2N5P7R9S1T3V5X",
    "display_name": "Northwind Office Supplies"
  },
  "currency": "GBP",
  "currency_rate": 1.0,
  "bill_date": "2025-01-08",
  "due_date": "2025-02-07",
  "po_number": "PO-2025-00188",
  "sub_total": 1240.00,
  "total_tax": 248.00,
  "total": 1488.00,
  "balance": 0.00,
  "status": "paid",
  "line_items": [
    {
      "description": "Ergonomic chairs x4",
      "unit_price": 310.00,
      "quantity": 4,
      "total_amount": 1240.00,
      "ledger_account": {
        "id": "acct_01HQAZ6E2J7L1N4P6R8S0T2V4W",
        "name": "Office Equipment"
      },
      "purchase_order": {
        "id": "po_01HQAZB0P6Q1R5T8V0X2Z4C6E8",
        "line_id": "pol_01HQAZB1Q7R2S6T9V1X3Z5C7E9"
      },
      "tracking_categories": [
        { "id": "dim_dept_01HQAZ5D1H6K0M3N5P7R9T1V3", "name": "Operations" }
      ]
    }
  ],
  "updated_at": "2025-01-15T11:03:22Z"
}
```

Repeat for [`GET /accounting/expenses`](/apis/accounting/reference#operation/expensesAll). Expenses carry a `transaction_date` and are generally already settled when they are read, so they skip the payment allocation step. Expense lines hold the ledger account in `line_items[].account` rather than `ledger_account`.

### 4. Pull bill payments

Payment behavior metrics (days-to-pay, on-time rate) need the settlement date of each bill, not only its `status`. The bill's `balance` tells you whether it is settled, and `paid_date` is not populated on every connector, so the payment records are the dependable source.

Pull [`GET /accounting/bill-payments`](/apis/accounting/reference#operation/billPaymentsAll) and join on `allocations[].id` where `allocations[].type == "bill"`:

```json
{
  "id": "bpay_01HQAZ9H5N0P4Q7S9U1W3Y5A7B",
  "transaction_date": "2025-02-05T00:00:00Z",
  "total_amount": 1488.00,
  "currency": "GBP",
  "supplier": {
    "id": "sup_01HQAZ7F3K8M2N5P7R9S1T3V5X",
    "display_name": "Northwind Office Supplies"
  },
  "allocations": [
    {
      "id": "bill_01HQAZ8G4M9N3P6R8T0V2W4Y6Z",
      "type": "bill",
      "amount": 1488.00
    }
  ],
  "updated_at": "2025-02-05T14:28:11Z"
}
```

Partial payments spread across multiple records. For the final payment date on a bill, pick the `transaction_date` of the payment that drives the bill `balance` to zero.

### 5. Pull purchase orders

Purchase orders feed PO-compliance analytics: what share of spend was pre-approved on a PO, and whether billed quantity and price match the PO line. Call [`GET /accounting/purchase-orders`](/apis/accounting/reference#operation/purchaseOrdersAll) and index by `id`, `po_number` and supplier.

```json
{
  "id": "po_01HQAZB0P6Q1R5T8V0X2Z4C6E8",
  "po_number": "PO-2025-00188",
  "supplier": {
    "id": "sup_01HQAZ7F3K8M2N5P7R9S1T3V5X",
    "display_name": "Northwind Office Supplies"
  },
  "issued_date": "2025-01-02",
  "expected_arrival_date": "2025-01-10",
  "currency": "GBP",
  "sub_total": 1240.00,
  "total": 1488.00,
  "status": "closed",
  "line_items": [
    {
      "id": "pol_01HQAZB1Q7R2S6T9V1X3Z5C7E9",
      "description": "Ergonomic chairs x4",
      "unit_price": 310.00,
      "quantity": 4,
      "total_amount": 1240.00
    }
  ]
}
```

Purchase orders are not available on every connector. Several of the mainstream accounting systems used elsewhere in this guide (Exact Online and its NL/UK variants, Sage Business Cloud, Sage Intacct, FreshBooks, Moneybird) do not expose purchase orders in coverage. Check the connector table below before promising PO compliance as a feature for a given customer.

### 6. Aggregate spend

With dimensions, suppliers, bills, expenses and payments loaded, aggregation is a group-by over the transaction set. The useful axes are supplier, ledger account (category), tracking category (department or project) and period.

```json
{
  "period": "2025-Q1",
  "supplier_id": "sup_01HQAZ7F3K8M2N5P7R9S1T3V5X",
  "ledger_account_id": "acct_01HQAZ6E2J7L1N4P6R8S0T2V4W",
  "department_id": "dim_dept_01HQAZ5D1H6K0M3N5P7R9T1V3",
  "currency": "GBP",
  "bill_total": 4712.00,
  "expense_total": 318.50,
  "total_spend": 5030.50,
  "transaction_count": 7
}
```

The line-item currency is the document currency. If the customer reports in a different base currency, convert using the document's `currency_rate` where present, or pull the company's base rate separately.

### 7. Derive vendor behavior metrics

Four signals come out of the data without extra endpoint calls:

| Metric | How to derive it |
| --- | --- |
| Days-to-pay | Difference between a bill's `bill_date` and the `transaction_date` of the payment that settles it. Average per supplier and per period. |
| On-time payment rate | Share of settled bills where the final payment `transaction_date` is on or before `due_date`. |
| Price drift | Group bill line items by `(supplier.id, item.id)`, or by `description` or a cleaned product key when `item` is absent. Order by `bill_date` and compare `unit_price` across consecutive bills. A sustained positive delta is a vendor price increase. |
| PO compliance | Share of bill total where a line carries `purchase_order.id`, or the bill's `po_number` matches a PO for the same supplier. Line-level compliance compares bill line `unit_price` and `quantity` against the PO line named by `purchase_order.line_id`. |

### 8. Deduplicate suppliers

Customers routinely create the same vendor two or three times, especially when migrating ledgers or when users bypass procurement. Deduplication runs on the suppliers payload before any aggregation.

A workable heuristic, in order of strength:

1. Exact match on `tax_number` after stripping whitespace and non-alphanumerics.
2. Exact match on normalized `company_name` or `display_name` (lowercased, punctuation stripped, legal suffixes like `ltd`, `inc`, `gmbh`, `bv` removed).
3. Fuzzy match (Jaro-Winkler or trigram) on normalized name with a confidence threshold, combined with matching country from `addresses[].country`.
4. Shared `bank_accounts[].iban` or `bank_accounts[].account_number` where the connector returns them.

Store the resulting canonical supplier ID alongside the raw `suppliers.id`, and roll bills and expenses up to the canonical ID before aggregating.

### 9. Keep the dataset fresh

Suppliers, bills, bill payments, expenses, purchase orders and ledger accounts accept `filter[updated_since]`. Tracking categories take no filters, so re-pull them in full. Connector support for the filter varies, so check the coverage matrix for each connector you enable. Persist the maximum `updated_at` you've seen for each resource per consumer, and pass it on the next run:

```http
GET /accounting/bills?filter[updated_since]=2025-02-05T14:28:11Z&limit=200 HTTP/1.1
```

Run suppliers, bills, bill payments, expenses and purchase orders on their own cursors. Bill payments can arrive out of order relative to bills, so re-join payments to bills on each run rather than caching the join.

>
> This use case is read-only. The pipeline never calls `POST`, `PATCH`, or `DELETE`
> on the Accounting API. Any write-back of enriched spend data goes to the
> customer's analytics warehouse, not to their accounting system.

## Rate limits and backfill

The first run for a new customer is a full backfill and the most rate-limit-sensitive moment in the integration. A few guardrails:

- Page with `limit=200`, the maximum these list endpoints allow, to minimize request count.
- Fan out across resources with bounded concurrency per consumer (two to four parallel streams is a safe default). Fanning out across consumers is fine; stacking parallel streams per consumer is what trips downstream connector limits.
- Watch for `429` responses on list endpoints and back off exponentially. A `429` with `type_name: ConnectorRateLimitError` means the downstream system's own per-minute or per-day ceiling was hit, even when your overall request rate looks modest.
- Persist cursors after each page, not only at the end of the backfill, so an interrupted run resumes without replaying the whole history.
- After backfill, incremental runs on `filter[updated_since]` keep per-run request counts proportional to actual ledger activity.

## Connector-specific behavior

The table covers each accounting connector and what it supports for this guide, based on the coverage matrix at the time of writing. Rows with no supplier or bill coverage should be filtered out of the connector list exposed to the analytics product.

| Connector | Notes |
| --- | --- |
| Access Financials | Suppliers, ledger accounts and tracking categories available. Bills, bill payments, expenses and purchase orders are not in coverage, so spend aggregation is limited to vendor master data. |
| Acumatica | Suppliers, bills, ledger accounts and purchase orders available. Bill payments, expenses and tracking categories are not in coverage, so days-to-pay is not derivable and dimensional slicing falls back to the chart of accounts. |
| Banqup | None of the resources used in this guide are in coverage. Skip for spend analytics. |
| Campfire | Suppliers, bills, ledger accounts and tracking categories available. Bill payments, expenses and purchase orders are not in coverage, so days-to-pay and PO compliance are not available. |
| Clear Books UK | Suppliers, bills and ledger accounts available. Bill payments, expenses and purchase orders are not in coverage, so payment behavior metrics are not available. |
| Digits | Suppliers, ledger accounts and tracking categories available. Bills, bill payments, expenses and purchase orders are not in coverage, so transaction-level spend aggregation is not possible. |
| DualEntry | Suppliers, bills, bill payments, expenses, purchase orders and ledger accounts available. Tracking categories not in coverage. Good fit for most of this guide. |
| Exact Online | Suppliers, bills, bill payments and ledger accounts available. Expenses and purchase orders are not in coverage, so paid-at-point-of-sale card spend and PO compliance will not surface. |
| Exact Online NL | Same shape as Exact Online. |
| Exact Online UK | Same shape as Exact Online. |
| FreeAgent | Suppliers, bills, bill payments, expenses and ledger accounts available. Purchase orders and tracking categories are not in coverage. |
| FreshBooks | Suppliers, bills, bill payments and ledger accounts available. Purchase orders, expenses and tracking categories are not in coverage, so PO compliance and dimensional slicing are not available. |
| Intuit Enterprise Suite | Every resource used here is available. Departments and locations are also available as separate resources. |
| KashFlow | Suppliers and ledger accounts only. Not suitable for transaction-level spend. |
| Microsoft Dynamics 365 Business Central | Suppliers, bills, bill payments, expenses, purchase orders, ledger accounts and tracking categories available. Good fit. |
| Moneybird | Suppliers, bills, expenses, ledger accounts and tracking categories available. Bill payments cannot be listed, so days-to-pay falls back to the `balance` snapshot approach described for Pennylane. Purchase orders are not in coverage. |
| MRI Software | Suppliers, bills, purchase orders and ledger accounts available. Bill payments, expenses and tracking categories are not in coverage. |
| MYOB | Bills, bill payments and ledger accounts available. Suppliers are not in coverage, so dedup must run on the `supplier` object embedded on each bill. Expenses, purchase orders and tracking categories are not in coverage. |
| MYOB Acumatica | Suppliers, bills, ledger accounts and purchase orders available. Bill payments, expenses and tracking categories are not in coverage, so days-to-pay is not available. |
| NetSuite | Every resource used here is available. Departments, locations and subsidiaries are available as separate resources for multi-entity customers. |
| Odoo | Suppliers, bills, bill payments, expenses, purchase orders, ledger accounts and tracking categories all available. |
| Pennylane | Suppliers, bills, purchase orders, ledger accounts and tracking categories available. Bill payments and expenses are not in coverage, so days-to-pay has to be inferred from `balance` crossing zero between `updated_at` snapshots. |
| Procountor | Suppliers, bills, purchase orders and ledger accounts available. Bill payments are not in coverage, so days-to-pay and on-time rate are not derivable. No expenses or tracking categories in coverage. |
| QuickBooks | Every resource used here is available. Classes map to tracking categories, and departments and locations are available as separate resources. |
| Rillet | Suppliers, bills, expenses and ledger accounts available. Bill payments can be created but not read, so days-to-pay is not derivable. Purchase orders and tracking categories not in coverage. |
| Sage Business Cloud Accounting | Suppliers, bills, bill payments, expenses and ledger accounts available. Purchase orders and tracking categories are not in coverage, so PO compliance and dimensional slicing are not available. |
| Sage Intacct | Suppliers, bills, bill payments, expenses, ledger accounts and tracking categories all available, plus departments, locations and subsidiaries. Purchase orders are not in coverage, so PO compliance is not available on this connector. |
| Sage Intacct REST | Same resource coverage as Sage Intacct. Purchase orders are not in coverage. |
| Stripe | Suppliers, bills and other AP resources are not in coverage. Not useful for AP spend analytics. |
| Visma Netvisor | Suppliers, bills and purchase orders available. Bill payments, ledger accounts and tracking categories are not in coverage, so days-to-pay and category slicing are not derivable. |
| Wave | Suppliers and ledger accounts only. Bills are not in coverage; skip for spend aggregation. |
| Workday | Suppliers, bills, bill payments, purchase orders and ledger accounts available. Expenses cannot be listed. Departments and subsidiaries are available; locations are not in coverage. |
| Xero | Suppliers, bills, bill payments, expenses, purchase orders, ledger accounts and tracking categories all available. Tracking categories are read-only on Xero. |
| Yuki | Suppliers, bills, ledger accounts and tracking categories available. Bill payments, expenses and purchase orders are not in coverage, so payment metrics and PO compliance are not available. |
| Zoho Books | Suppliers, bills, bill payments, expenses, ledger accounts and purchase orders all available. Tracking categories are not in coverage, so dimensional slicing falls back to ledger account and supplier only. |

### MYOB

With no `suppliers` endpoint on this connector, the vendor list is derived from the distinct supplier objects embedded on bills and bill payments. The dedup pass runs on `supplier.display_name` normalized the same way as the standard suppliers payload, then promotes the chosen canonical name into the analytics layer's own supplier dimension.

### Pennylane

Pennylane does not expose `bill-payments`, so days-to-pay and on-time rate have to be approximated. Snapshot each bill's `balance` on every incremental run and record the first run where `balance` reaches zero. The bill's `updated_at` at that point is an upper bound on the settlement date, and the precision depends on how often you run.

### Xero

Tracking categories are read-only on Xero. If the customer has not configured a tracking category for the dimension the analytics product wants to slice by (for example, department), that dimension will not be populated on their bills. Fall back to splitting by ledger account alone for those customers.

### Zoho Books

Zoho Books covers suppliers, bills, bill payments, expenses and purchase orders, but tracking categories are not in coverage. Department, class or project slicing is not available on this connector; group spend by ledger account and supplier only.

### NetSuite and Sage Intacct

Both expose multi-dimensional data (departments, locations and subsidiaries). For multi-entity customers, pull subsidiaries first and tag every bill with its `subsidiary.id` so cross-entity spend can be rolled up or filtered; `filter[subsidiary_id]` on bills scopes a pull to one entity. Expenses carry no subsidiary field, so attribute them through `department` or `location` where the customer uses those. For single-entity customers only tracking categories matter. Purchase orders are not in coverage on either Sage Intacct variant, so PO compliance analytics for those customers is not available.

## Next steps

- [Accounting API reference](/apis/accounting/reference)
- [Managing Locations, Departments, and Subsidiaries](/guides/locations-subsidiaries-departments)
- [Handling Bills and Expenses](/guides/expenses-bills) for the write-side companion to this read-only guide
- [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid) if the analytics product also drives payment actions
