# Procurement with the Accounting API

By the end of this page you'll have raised a purchase order against a supplier, converted it to a bill when the goods arrived, settled that bill with an allocated payment, and read the outstanding payables back, inside a customer's QuickBooks, Xero, or NetSuite. One integration against the unified [Accounting API](/apis/accounting/reference), three writes, about ten minutes.

_Your product sends purchase orders, bills and bill payments to the Apideck Accounting API, which writes them into whichever ledger your customer uses and returns the outstanding payables._

Supplier, ledger account, and tax rate lookups are the same calls on every connector, one PO and one bill schema replace the per-vendor shapes, [Vault](/guides/vault) handles the connection lifecycle and token refresh, and on the four connectors that expose it, `aged-creditors` returns the downstream system's own payables bucketing instead of you reaggregating bills.

## Resource mapping

Procurement spans three unified resources. The table below shows what each one resolves to in the most common downstream systems, and where the resource is absent from coverage entirely.

| Connector | Purchase order | Bill | Outstanding payables |
| --- | --- | --- | --- |
| QuickBooks | `PurchaseOrder` | `Bill` | `aged-creditors` (A/P aging) |
| Xero | `Purchase Order` | `Bill` (ACCPAY invoice) | `aged-creditors` (Aged Payables) |
| NetSuite | `Purchase Order` | `Vendor Bill` | Not in coverage, sum open bills |
| Sage Intacct | Not in coverage | `AP Bill` | Not in coverage, sum open bills |
| Exact Online | Not in coverage | `Purchase Invoice` | Not in coverage, sum open bills |
| Microsoft Dynamics 365 Business Central | `Purchase Order` | `Purchase Invoice` | Not in coverage, sum open bills |
| Odoo | `purchase.order` (read only) | `account.move` (vendor bill) | Not in coverage, sum open bills |

## Walkthrough

The flow is the same on every connector that supports the whole path: resolve the supplier and the expense account, raise the PO, convert it to a bill on receipt, then settle the bill with an allocated payment.

Each step below shows the raw request body first, then the same call through the [`@apideck/unify`](/sdks/node) SDK. The SDK takes camelCase and handles serialization, so pick whichever fits your stack. Every snippet is verified against `@apideck/unify` v0.46.0.

```ts
import { Apideck } from '@apideck/unify'

const apideck = new Apideck({
  apiKey: process.env.API_KEY!,
  appId: process.env.APP_ID!,
  consumerId: 'user_01H8X9Y2A3B4C5D6E7F8G9H0'
})
```

### 1. Look up the supplier and expense account

Before raising a purchase order, resolve the supplier and the ledger account the line items will post to. Both are exposed as unified resources.

```json
{
  "filter": {
    "company_name": "Northwind Office Supplies"
  }
}
```

Send this to [`GET /accounting/suppliers`](/apis/accounting/reference#operation/suppliersAll) with the headers below. Use the same headers on every other call in this guide.

```http
GET /accounting/suppliers?filter[company_name]=Northwind%20Office%20Supplies HTTP/1.1
Host: unify.apideck.com
Authorization: Bearer ${APIDECK_API_KEY}
x-apideck-app-id: ${APIDECK_APP_ID}
x-apideck-consumer-id: user_01H8X9Y2A3B4C5D6E7F8G9H0
x-apideck-service-id: quickbooks
```

Fetch the matching expense account from [`GET /accounting/ledger-accounts`](/apis/accounting/reference#operation/ledgerAccountsAll) using `filter[classification]=expense`. The filter is `classification`, not `type`. Cache both IDs against the supplier in your own database so subsequent POs skip the lookup.

Or through the SDK, guarding both ids before you use them as linked references:

```ts
const { getSuppliersResponse } = await apideck.accounting.suppliers.list({
  serviceId: 'quickbooks',
  filter: { companyName: 'Northwind Office Supplies' },
  limit: 1
})

const supplierId = getSuppliersResponse?.data?.[0]?.id
if (!supplierId) throw new Error('No supplier matched, create one before raising the PO')

const { getLedgerAccountsResponse } = await apideck.accounting.ledgerAccounts.list({
  serviceId: 'quickbooks',
  filter: { classification: 'expense', name: 'Office Supplies' },
  limit: 200
})

const expenseAccountId = getLedgerAccountsResponse?.data?.[0]?.id
if (!expenseAccountId) throw new Error('No expense account matched')
```

`limit` caps at 200. Of the connectors here, only QuickBooks and `sage-intacct-rest` support the `name` filter on ledger accounts, so on Xero, NetSuite, and legacy Sage Intacct you page the chart of accounts with `meta.cursors.next` and match locally. A miss returns `undefined`, which is how an uncoded line ends up posted silently.

### 2. Create the purchase order

With supplier and account IDs in hand, raise the PO. Keep the line items close to what the buyer actually requested. Most downstream systems will copy these straight onto the bill in the next step.

```json
{
  "po_number": "PO-2025-00417",
  "supplier": {
    "id": "sup_01H8X9Y2A3B4C5D6E7F8G9H0"
  },
  "issued_date": "2025-03-04",
  "delivery_date": "2025-03-18",
  "currency": "USD",
  "status": "open",
  "line_items": [
    {
      "description": "Ergonomic desk chair",
      "quantity": 6,
      "unit_price": 289.0,
      "total_amount": 1734.0,
      "item": {
        "id": "item_01H8X9Y2A3B4C5D6E7F8G9H5"
      }
    },
    {
      "description": "Standing desk converter",
      "quantity": 6,
      "unit_price": 145.0,
      "total_amount": 870.0,
      "item": {
        "id": "item_01H8X9Y2A3B4C5D6E7F8G9H6"
      }
    }
  ],
  "total_tax": 234.36,
  "total": 2838.36,
  "memo": "Net 30 terms, deliver to NYC office"
}
```

Send this to [`POST /accounting/purchase-orders`](/apis/accounting/reference#operation/purchaseOrdersAdd). The supplier is a linked object, `supplier: { id }`, not a `supplier_id` string. `total` is the gross figure the bill and the payment must agree with, so if you add tax at line level rather than in `total_tax`, make the header total gross or set `tax_inclusive`. Store the returned PO ID against your internal procurement record so it can be correlated with the bill later.

These calls are pinned to QuickBooks, where PO lines are item-driven: the line carries `item` and the expense account you resolved in step 1 is coded on the bill in step 3 instead, since QuickBooks bill lines do accept `ledger_account`. On connectors that expose `ledger_account` on PO lines, such as Business Central and Acumatica, you can code the account on the PO as well.

Or through the SDK:

```ts
const { createPurchaseOrderResponse } = await apideck.accounting.purchaseOrders.create({
  serviceId: 'quickbooks',
  purchaseOrder: {
    poNumber: 'PO-2025-00417',
    supplier: { id: supplierId },
    issuedDate: new Date('2025-03-04'),
    deliveryDate: new Date('2025-03-18'),
    currency: 'USD',
    status: 'open',
    lineItems: [
      {
        description: 'Ergonomic desk chair',
        quantity: 6,
        unitPrice: 289,
        totalAmount: 1734,
        item: { id: 'item_01H8X9Y2A3B4C5D6E7F8G9H5' }
      },
      {
        description: 'Standing desk converter',
        quantity: 6,
        unitPrice: 145,
        totalAmount: 870,
        item: { id: 'item_01H8X9Y2A3B4C5D6E7F8G9H6' }
      }
    ],
    totalTax: 234.36,
    total: 2838.36,
    memo: 'Net 30 terms, deliver to NYC office'
  }
})

const purchaseOrderId = createPurchaseOrderResponse?.data?.id
if (!purchaseOrderId) throw new Error('Purchase order was not created')
```

Date-only fields such as `issuedDate` and `deliveryDate` are plain `Date` objects in the SDK. There is no `RFCDate` export.

### 3. Convert the PO to a bill on receipt

When goods arrive (or services are confirmed), turn the PO into a bill. Carry the PO number through the bill's `po_number` field so connectors and downstream users can correlate the two. For partial receipts, copy only the received line items and keep the PO open for the rest.

```json
{
  "bill_number": "NW-INV-9418",
  "supplier": {
    "id": "sup_01H8X9Y2A3B4C5D6E7F8G9H0"
  },
  "po_number": "PO-2025-00417",
  "bill_date": "2025-03-18",
  "due_date": "2025-04-17",
  "currency": "USD",
  "status": "authorised",
  "line_items": [
    {
      "description": "Ergonomic desk chair",
      "quantity": 6,
      "unit_price": 289.0,
      "total_amount": 1734.0,
      "ledger_account": {
        "id": "acct_01H8X9Y2A3B4C5D6E7F8G9H1"
      }
    },
    {
      "description": "Standing desk converter",
      "quantity": 6,
      "unit_price": 145.0,
      "total_amount": 870.0,
      "ledger_account": {
        "id": "acct_01H8X9Y2A3B4C5D6E7F8G9H1"
      }
    }
  ],
  "sub_total": 2604.0,
  "total_tax": 234.36,
  "total": 2838.36
}
```

Send this to [`POST /accounting/bills`](/apis/accounting/reference#operation/billsAdd). QuickBooks, Xero, NetSuite, and Business Central all expose `po_number` on the bill. Neither Sage Intacct connector does, so put the PO number in `reference` there. Keep `status` at `authorised` rather than `draft`: a draft bill is unposted and will not accept the payment allocation in the next step. Note that QuickBooks carries neither `status` nor `sub_total` in its supported field set for bills — it derives both itself, so the values you send are dropped there. Keep them in the payload anyway: they are load-bearing on Xero and Sage Intacct, so the same body stays portable even though this example is pinned to QuickBooks.

Or through the SDK:

```ts
const { createBillResponse } = await apideck.accounting.bills.create({
  serviceId: 'quickbooks',
  bill: {
    billNumber: 'NW-INV-9418',
    supplier: { id: supplierId },
    poNumber: 'PO-2025-00417',
    billDate: new Date('2025-03-18'),
    dueDate: new Date('2025-04-17'),
    currency: 'USD',
    status: 'authorised',
    lineItems: [
      {
        description: 'Ergonomic desk chair',
        quantity: 6,
        unitPrice: 289,
        totalAmount: 1734,
        ledgerAccount: { id: expenseAccountId }
      },
      {
        description: 'Standing desk converter',
        quantity: 6,
        unitPrice: 145,
        totalAmount: 870,
        ledgerAccount: { id: expenseAccountId }
      }
    ],
    subTotal: 2604,
    totalTax: 234.36,
    total: 2838.36
  }
})

const billId = createBillResponse?.data?.id
if (!billId) throw new Error('Bill was not created')
```

After this call, the PO is fulfilled (fully or partially) and a payable exists in the customer's books.

### 4. Pay the bill and close the loop

When the buyer pays, post a bill payment with an allocation that points back at the bill. The downstream system flips the bill status to paid for you.

```json
{
  "currency": "USD",
  "total_amount": 2838.36,
  "transaction_date": "2025-04-15T09:00:00.000Z",
  "payment_method": "ach",
  "reference": "ACH-2025-04-15-0099",
  "supplier": {
    "id": "sup_01H8X9Y2A3B4C5D6E7F8G9H0"
  },
  "account": {
    "id": "acct_01H8X9Y2A3B4C5D6E7F8G9H4"
  },
  "type": "accounts_payable",
  "status": "authorised",
  "reconciled": true,
  "allocations": [
    {
      "id": "bill_01H8X9Y2A3B4C5D6E7F8G9H3",
      "type": "bill",
      "amount": 2838.36
    }
  ]
}
```

Send this to [`POST /accounting/bill-payments`](/apis/accounting/reference#operation/billPaymentsAdd). `total_amount` and `transaction_date` are the two required fields. For deeper detail on the allocations contract, see the [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid) guide.

Or through the SDK:

```ts
await apideck.accounting.billPayments.create({
  serviceId: 'quickbooks',
  billPayment: {
    currency: 'USD',
    totalAmount: 2838.36,
    transactionDate: new Date('2025-04-15T09:00:00.000Z'),
    paymentMethod: 'ach',
    reference: 'ACH-2025-04-15-0099',
    supplier: { id: supplierId },
    account: { id: 'acct_01H8X9Y2A3B4C5D6E7F8G9H4' },
    type: 'accounts_payable',
    status: 'authorised',
    reconciled: true,
    allocations: [{ id: billId, type: 'bill', amount: 2838.36 }]
  }
})
```

Drop the `allocations` array and the cash posts against the supplier while the bill stays open, which is the most common payables bug.

### 5. Track outstanding payables

To show buyers what they still owe, call the dedicated aged creditors report rather than aggregating open bills. The downstream system applies its own rounding, partial-payment handling, and bucket boundaries, which raw bill data will not match exactly.

```
GET https://unify.apideck.com/accounting/aged-creditors?filter[report_as_of_date]=2025-04-30&filter[period_count]=4&filter[period_length]=30
```

Or through the SDK. Note that `reportAsOfDate` is a string, not a `Date`:

```ts
const { getAgedCreditorsResponse } = await apideck.accounting.agedCreditors.get({
  serviceId: 'quickbooks',
  filter: { reportAsOfDate: '2025-04-30', periodCount: 4, periodLength: 30 }
})

const payables = getAgedCreditorsResponse?.data?.outstandingBalances ?? []
void payables
```

See [Aged Creditors one](/apis/accounting/reference#operation/agedCreditorsOne). The response contains supplier-level totals in `outstanding_balances`, bucketed by age.

>
> Only four connectors expose `aged-creditors`: QuickBooks, Xero, Intuit Enterprise Suite, and AFAS (AFAS without any of the report filters). Everywhere else, build the buckets yourself from bills.

Falling back to bills means [`GET /accounting/bills`](/apis/accounting/reference#operation/billsAll) with `filter[updated_since]`, then bucketing on `due_date` and `balance` in your own code. `filter[status]` exists in the unified schema but is not in coverage on any of the connectors in the mapping table above, so do not rely on it to return only open bills.

## What actually bites people

Three constraints on the connectors most procurement products start with, each verified against the live coverage matrix rather than the unified schema.

**QuickBooks Online.** Purchase orders support no filters and no sorting at all: not `updated_since`, not `supplier_id`. To find a supplier's POs you page the entire list with `meta.cursors.next` and index on your side. QuickBooks PO line items carry no `ledger_account`, only `item`, which is why the PO above codes `item: { id }` and the expense account you resolved in step 1 first appears on the bill, where `line_items.ledger_account.id` is in coverage. `reference` and `sub_total` are not in QuickBooks PO coverage either, so the PO above omits both and uses `memo` for the buyer-facing note.

**Xero.** The PO line `ledger_account` is exposed only as `code`, never `id`, on both purchase orders and bills, so `ledger_account: { id }` silently loses the coding on Xero and you have to send the account code instead. The Xero PO exposes only `supplier.id` with no `display_name`, so resolve the supplier before you build the payload. Xero bill payments have no update operation: to correct one you delete it and post a new one.

**Sage Intacct.** Neither `sage-intacct` nor `sage-intacct-rest` supports the `purchase-orders` resource, so on Intacct the flow starts at the bill and your PO stays in your own product. Bills, bill payments, and suppliers are all writable on both. Legacy `sage-intacct` bill lines carry no `quantity` or `unit_price`, only `description`, `total_amount`, and `ledger_account`, so compute extended amounts yourself, and its supplier lookup filters on `display_name` only, not `company_name`. The newer `sage-intacct-rest` connector is the richer one here: it adds `company_name`, `first_name`, and `last_name` supplier filters, `sub_total`, and `payment_allocations` on bills, though neither connector exposes `total_tax`.

## Connector-specific behavior

Coverage below is what the live matrix reports for `purchase-orders`, `bills`, `bill-payments`, and `suppliers` on each connector. Writable means the resource accepts a create through Apideck, read only means it lists but cannot be written, and not in coverage means the resource is absent for that connector.

| Connector | Notes |
| --- | --- |
| Access Financials | Suppliers writable. Not in coverage: POs, bills, bill payments. |
| Acumatica | POs, bills, and suppliers writable. No bill payments. |
| Banqup | None of the four resources are in coverage. |
| Campfire | Bills and suppliers writable. Bill payments read only, no POs. |
| Clear Books | Bills and suppliers read only. No POs or bill payments. |
| Digits | Suppliers read only. Nothing else in coverage. |
| DualEntry | Full flow writable: suppliers, POs, bills, bill payments. |
| Exact Online | Bills map to purchase invoices and are writable. Suppliers read only, no POs. |
| Exact Online (NL) | Same as Exact Online. Bills and bill payments writable, suppliers read only. |
| Exact Online (UK) | Bills are read only here, unlike the other two Exact connectors. Bill payments writable. |
| FreeAgent | Bills and suppliers read only. No POs or bill payments. |
| FreshBooks | Suppliers and bill payments writable. Bills cannot be created, no POs. |
| Intuit Enterprise Suite | Full flow writable, and one of the four connectors exposing `aged-creditors`. |
| KashFlow | Suppliers read only. Nothing else in coverage. |
| Microsoft Dynamics 365 Business Central | Full flow writable. POs and posted purchase invoices are separate documents, so carry the number in the bill's `po_number`. Tracking categories supported on both. |
| Moneybird | Bills and suppliers writable, bill payments create only, no POs. |
| MRI Software | POs read only. Bills and suppliers create only. No bill payments. |
| MYOB | Bills and bill payments only. Suppliers and POs are not in coverage. |
| MYOB Acumatica | POs, bills, and suppliers writable. No bill payments. |
| NetSuite | Full flow writable. Bills map to vendor bills. Scope with `subsidiary_id` on the PO and `company_id` on the bill: tracking categories are not in NetSuite coverage for either. |
| Odoo | POs read only. Bills and bill payments create only, and bills map to `account.move` vendor bills. |
| Pennylane | POs create only. Bills list and update but cannot be created. No bill payments. |
| Procountor | POs, bills, and suppliers writable. No bill payments. |
| QuickBooks | Full flow writable, plus `aged-creditors`. See above for the PO filter and line-coding limits. |
| Rillet | Bills, bill payments, and suppliers writable. No POs. |
| Sage Business Cloud Accounting | Bills and suppliers writable, bill payments create only, no POs. |
| Sage Intacct | No POs. Bills, bill payments, and suppliers writable, and dimensions flow through `line_items.tracking_categories`. |
| Sage Intacct (REST) | Also no POs. Richer supplier filters and `payment_allocations` on bills, but no `total_tax` or supplier bank accounts. |
| Stripe | A payments platform rather than a procurement system. None of the four resources are in coverage. |
| Visma Netvisor | POs and suppliers writable, bills create only, no bill payments. |
| Wave | Suppliers read only. Nothing else in coverage. |
| Workday | POs, bills, and suppliers writable. Bill payments read only. |
| Xero | Full flow writable, plus `aged-creditors`. Bills map to ACCPAY invoices. Line-level tracking categories on POs but not on bills. |
| Yuki | Suppliers writable, bills read only. No POs or bill payments. |
| Zoho Books | Full flow writable. No aged creditors report. |

### NetSuite

For multi-subsidiary tenants, every PO and bill needs subsidiary context. NetSuite purchase orders expose `subsidiary_id` and `company_id`, and NetSuite bills expose `company_id`, so persist the chosen scope against the Apideck consumer and set it at creation time rather than looking it up per request. The unified `subsidiary` object is not writable on NetSuite records. See [Managing Locations, Departments, and Subsidiaries](/guides/locations-subsidiaries-departments) for the full pattern.

### Microsoft Dynamics 365 Business Central

Business Central distinguishes between the purchase order and the posted purchase invoice. The unified flow stays the same, and the bill becomes visible to your payables view once the invoice has been posted in the customer's tenant.

## Visit the demo

The procurement demo runs this whole path against a live connected ledger: it syncs suppliers, raises a purchase order, three-way matches the receipt against the supplier invoice, then posts the approved bill and reads spend back by supplier and category. Pick a ledger, watch each request go out, nothing to set up.

## Next steps

- [Handling Bills and Expenses](/guides/expenses-bills) for the broader payables story including expense reports and receipts.
- [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid) for the allocations contract on bill payments.
- [Accounting API reference](/apis/accounting/reference) for the full schema of every resource used above.
