# Accounts payable automation

Send an approved supplier invoice from your product into a customer's accounting system, then record a completed payment against the bill. This guide takes you from connector setup to a verified ledger record using the unified [Accounting API](/apis/accounting/reference).

## Explore the demo

**Purchase-to-pay demo**

Follow purchasing from supplier selection through an approved bill.

Choose Mock for synthetic sample data. Live mode requires your own credentials and an authorized connection; requests can read or write real records. Check connector coverage before building.

[Open demo](https://demos.apideck.com/procurement) · [API endpoints](/apis/accounting/reference?useCase=accounts-payable)

Your application owns invoice capture, approvals, duplicate prevention and payment execution. Apideck provides the connection and normalized API calls. **A bill-payment record documents a payment; it does not transfer money.**

## Your build path

| Step                                      | What you build                                            | Done when                                         |
| ----------------------------------------- | --------------------------------------------------------- | ------------------------------------------------- |
| [Check support](#check-connector-support) | Confirm the operations and fields your connector supports | Required reads and writes are available           |
| [Connect](#connect-your-customers-ledger) | Create a Vault session and complete connector setup       | Your backend can read from the intended company   |
| [Map and create](#walkthrough)            | Resolve a supplier and account IDs, then create a bill    | You have stored the returned bill ID              |
| [Verify](#verify-the-result)              | Read the bill and inspect its ledger entry                | Supplier, currency, amounts and allocations match |
| [Operate](#production-checklist)          | Handle duplicates, retries and reconnection               | Failure cases have been tested                    |

## Check connector support

Open the [AP coverage matrix](/apis/accounting/coverage?resources=suppliers,ledger-accounts,bills,bill-payments,attachments). Select your customer's connector and check operations first, then switch to **Fields** to inspect request fields and filters. A resource appearing in the unified schema does not mean every connector implements every operation.

| Requirement                           | Operation reference                                                                                                                                           | If unavailable                                                                  |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Find an existing supplier             | [List suppliers](/apis/accounting/reference/suppliers#suppliersAll)                                                                                           | Resolve the supplier in the customer's accounting system before writing         |
| Add a new supplier, if needed         | [Create supplier](/apis/accounting/reference/suppliers#suppliersAdd)                                                                                          | Have the customer create it, then use its existing ID                           |
| Map expense and bank accounts         | [List ledger accounts](/apis/accounting/reference/ledger-accounts#ledgerAccountsAll)                                                                          | Confirm a supported way to obtain valid account references before proceeding    |
| Write and verify a bill               | [Create bill](/apis/accounting/reference/bills#billsAdd), [Get bill](/apis/accounting/reference/bills#billsOne)                                               | Scope the integration to supported reads; do not offer bill creation            |
| Record and verify a completed payment | [Create bill payment](/apis/accounting/reference/bill-payments#billPaymentsAdd), [Get bill payment](/apis/accounting/reference/bill-payments#billPaymentsOne) | Keep payment recording outside this workflow until a supported path is verified |
| Attach the source invoice, optional   | [Upload attachment](/apis/accounting/reference/attachments#attachmentsUpload)                                                                                 | Retain the document in your application and link it to your AP record           |

For agents, the same coverage is available as [suppliers Markdown](/apis/accounting/coverage/suppliers.md), [bills Markdown](/apis/accounting/coverage/bills.md) and [bill payments Markdown](/apis/accounting/coverage/bill-payments.md).

## Connect your customer's ledger

### Developer setup

1. Complete [Get Started](/get-started): create an application, enable Accounting and your connector, and obtain server-side credentials.
2. Map each customer or tenant to its own consumer ID. Keep the consumer, connector and company context together throughout this workflow.
3. Create a [Vault session on your backend](/guides/vault) and open the returned session in your frontend. Your API key stays on the backend.
4. After authorization, confirm all required connector settings are complete. Start with [List bills](/apis/accounting/reference/bills#billsAll) to verify access before performing a write. An empty list can be valid for an empty company.

### Customer administrator setup

1. Open the relevant [connector setup guide](/apis/accounting) and confirm who has permission to authorize the connection.
2. Choose the intended accounting company and grant the permissions required by your workflow.
3. Complete the connector's settings in Vault, including company or subsidiary selection when applicable.
4. Confirm the supplier, expense accounts, payment account, tax treatment and any tracking dimensions with the person responsible for the ledger.

Use a test company for your first write. Replace all sample IDs, account codes, dates and amounts below with values appropriate for that company. If authorization expires or settings change, send the customer back through Vault and verify the connection again.

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

## Resource mapping

The workflow joins **Supplier → Bill → Bill payment allocation**. Store the returned IDs with your own vendor, invoice and payment records, scoped to the customer and connector. A supplier name or bill number alone is not a unique cross-system identifier.

The downstream system's object names and required fields vary. Use the selected connector's coverage and setup documentation to confirm its mapping before adapting the examples.

## Walkthrough

The example below ingests a vendor invoice, codes its lines to expense accounts, and records a payment that clears the bill. All requests target `https://unify.apideck.com`.

Each step 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. These examples target `@apideck/unify` v0.52.2 and QuickBooks. Install that version to follow them (`npm install @apideck/unify@0.52.2`), or adapt the response shapes to your installed SDK using its [versioned documentation](https://github.com/apideck-libraries/sdk-typescript). Run the TypeScript steps sequentially in a server-side script; keep payment recording behind your application's payment-completed event.

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

function requiredEnv(name: string): string {
  const value = process.env[name]
  if (!value) throw new Error(`Missing ${name}`)
  return value
}

const apideck = new Apideck({
  apiKey: requiredEnv('APIDECK_API_KEY'),
  appId: requiredEnv('APIDECK_APP_ID'),
  consumerId: requiredEnv('APIDECK_CONSUMER_ID')
})
```

### 1. Resolve or create the supplier

First, reuse the supplier ID stored in your application. For an unmapped vendor, search by a supported filter and confirm the match before creating a supplier. Send this to [`GET /accounting/suppliers`](/apis/accounting/reference#operation/suppliersAll) with a filter:

```
GET /accounting/suppliers?filter[company_name]=Northwind%20Cloud%20Hosting
Authorization: Bearer ${APIDECK_API_KEY}
x-apideck-app-id: ${APIDECK_APP_ID}
x-apideck-consumer-id: ${APIDECK_CONSUMER_ID}
x-apideck-service-id: quickbooks
```

If the supplier doesn't exist, create one via [`POST /accounting/suppliers`](/apis/accounting/reference#operation/suppliersAdd):

```json
{
  "company_name": "Northwind Cloud Hosting",
  "display_name": "Northwind Cloud Hosting",
  "tax_number": "GB123456789",
  "currency": "USD",
  "addresses": [
    {
      "type": "primary",
      "line1": "455 Market Street",
      "line2": "Suite 1400",
      "city": "San Francisco",
      "state": "CA",
      "postal_code": "94105",
      "country": "US"
    }
  ],
  "emails": [{ "email": "ap@northwindcloud.example", "type": "primary" }],
  "phone_numbers": [{ "number": "+1 415 555 0188", "type": "primary" }],
  "payment_method": "ACH",
  "status": "active"
}
```

Through the SDK, inspect all matching suppliers before deciding whether to create one. This example rejects ambiguous name matches; production matching should also use stable identifiers from your vendor records.

```ts
const matches = new Set<string>()
for await (const page of await apideck.accounting.suppliers.list({
  serviceId: 'quickbooks',
  filter: { companyName: 'Northwind Cloud Hosting' },
  limit: 100
})) {
  for (const supplier of page.getSuppliersResponse?.data ?? []) {
    if (supplier.id && supplier.companyName === 'Northwind Cloud Hosting') {
      matches.add(supplier.id)
    }
  }
}
if (matches.size > 1) throw new Error('Confirm which supplier to use before continuing')
let supplierId = [...matches][0]
if (!supplierId) {
  const created = await apideck.accounting.suppliers.create({
    serviceId: 'quickbooks',
    supplier: {
      companyName: 'Northwind Cloud Hosting',
      displayName: 'Northwind Cloud Hosting',
      currency: 'USD',
      emails: [{ email: 'ap@northwindcloud.example', type: 'primary' }],
      status: 'active'
    }
  })
  supplierId = created.createSupplierResponse?.data?.id ?? ''
}
if (!supplierId) throw new Error('Could not resolve a supplier to bill')
```

Persist the returned supplier ID on your vendor record. Serialize onboarding for the same vendor in your application: searching before creating does not prevent two concurrent requests from creating duplicates.

### 2. Fetch the chart of accounts for line coding

Pull the expense accounts you'll use for bill lines using [`GET /accounting/ledger-accounts`](/apis/accounting/reference#operation/ledgerAccountsAll). Filter by classification to keep the response focused:

```
GET /accounting/ledger-accounts?filter[classification]=expense
```

Or through the SDK:

```ts
// Cache by nominal code so the bill lines below can look up an account ID
// without re-fetching. limit caps at 200 and a real chart of accounts is
// usually longer, so page through the async iterator rather than reading
// a single page.
const accountsByCode = new Map<string, string>()

for await (const page of await apideck.accounting.ledgerAccounts.list({
  serviceId: 'quickbooks',
  filter: { classification: 'expense' },
  limit: 200
})) {
  for (const account of page.getLedgerAccountsResponse?.data ?? []) {
    if (account.nominalCode && account.id) accountsByCode.set(account.nominalCode, account.id)
  }
}
```

Store the resulting account IDs in your category mapping, scoped to the customer and connector. The bill creation in step 3 checks these mappings before using them on each line. Resolve the payment account separately from the same company before recording a payment.

### 3. Ingest the bill

With the supplier and account IDs in hand, post the bill to [`POST /accounting/bills`](/apis/accounting/reference#operation/billsAdd):

```json
{
  "bill_number": "NW-2025-04412",
  "supplier": {
    "id": "acct_supp_01H8X9Y2A3B4C5D6E7F8G9H0J1"
  },
  "currency": "USD",
  "bill_date": "2025-03-04",
  "due_date": "2025-04-03",
  "reference": "PO-9921",
  "po_number": "PO-9921",
  "line_items": [
    {
      "description": "Production Kubernetes cluster, March 2025",
      "quantity": 1,
      "unit_price": 4200.0,
      "total_amount": 4200.0,
      "ledger_account": {
        "id": "acct_ledger_01HQR4F2T8YZA3KQVN9XJ7M5RP",
        "nominal_code": "6310",
        "name": "Cloud Hosting"
      },
      "tracking_categories": [{ "id": "trk_dept_eng", "name": "Engineering" }]
    },
    {
      "description": "Egress and CDN overage",
      "quantity": 1,
      "unit_price": 318.47,
      "total_amount": 318.47,
      "ledger_account": {
        "id": "acct_ledger_01HQR4F31KZB6F2N7P9Q8T0V2W",
        "nominal_code": "6315",
        "name": "Bandwidth"
      }
    }
  ],
  "sub_total": 4518.47,
  "total_tax": 0.0,
  "total": 4518.47,
  "status": "authorised"
}
```

Check the following before sending that body. The vendor is linked through the nested `supplier` object. Check supported bill states and posting requirements for your connector before allocating a payment; a draft bill may need approval or posting first. Required tax fields, dimensions and account references can differ by connector.

Or through the SDK:

```ts
const cloudHostingAccountId = accountsByCode.get('6310')
const bandwidthAccountId = accountsByCode.get('6315')
if (!cloudHostingAccountId || !bandwidthAccountId) {
  throw new Error('Chart of accounts is missing an expected expense account')
}

const { createBillResponse } = await apideck.accounting.bills.create({
  serviceId: 'quickbooks',
  bill: {
    billNumber: 'NW-2025-04412',
    supplier: { id: supplierId },
    currency: 'USD',
    billDate: new Date('2025-03-04'),
    dueDate: new Date('2025-04-03'),
    reference: 'PO-9921',
    poNumber: 'PO-9921',
    lineItems: [
      {
        description: 'Production Kubernetes cluster, March 2025',
        quantity: 1,
        unitPrice: 4200,
        totalAmount: 4200,
        ledgerAccount: { id: cloudHostingAccountId, nominalCode: '6310' },
        trackingCategories: [{ id: 'trk_dept_eng', name: 'Engineering' }]
      },
      {
        description: 'Egress and CDN overage',
        quantity: 1,
        unitPrice: 318.47,
        totalAmount: 318.47,
        ledgerAccount: { id: bandwidthAccountId, nominalCode: '6315' }
      }
    ],
    subTotal: 4518.47,
    totalTax: 0,
    total: 4518.47,
    status: 'authorised'
  }
})

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

The response returns the bill `id`. Store it alongside your internal AP record so payment events can be applied back to it. Note the date-only fields: `billDate` and `dueDate` take a plain `Date`, not the SDK's `RFCDate` helper (which some other resources, like CRM opportunities, use for their own date-only fields).

### 4. Attach the source document

For audit trail, upload the original PDF invoice with [Upload attachment](/apis/accounting/reference/attachments#attachmentsUpload), using `bill` as the `reference_type` and the bill ID as the `reference_id`. Attachment support is connector-dependent (check the AP coverage matrix); fall back to storing the file in your own object storage for connectors that don't accept uploads.

### 5. Record the bill payment

When the bill is paid, post a payment that allocates against it via [`POST /accounting/bill-payments`](/apis/accounting/reference#operation/billPaymentsAdd):

```json
{
  "transaction_date": "2025-04-02T16:00:00Z",
  "supplier": {
    "id": "acct_supp_01H8X9Y2A3B4C5D6E7F8G9H0J1",
    "display_name": "Northwind Cloud Hosting"
  },
  "currency": "USD",
  "total_amount": 4518.47,
  "reference": "ACH-2025-04-02-00471",
  "payment_method": "ACH",
  "account": {
    "id": "acct_ledger_01HQR4F4P9R2VX5N6T8Q1W3Y4Z",
    "nominal_code": "1100",
    "name": "Operating Bank Account"
  },
  "type": "accounts_payable",
  "status": "authorised",
  "reconciled": false,
  "allocations": [
    {
      "id": "acct_bill_01HQR8M5Y2K3X7N6P9Q1V4T8R5",
      "type": "bill",
      "amount": 4518.47
    }
  ]
}
```

Use the bill ID returned by step 3, a payment account from the same company, and the amount actually paid. Check supported payment fields before adapting this payload to another connector. Through the SDK:

```ts
const { createBillPaymentResponse } = await apideck.accounting.billPayments.create({
  serviceId: 'quickbooks',
  billPayment: {
    transactionDate: new Date('2025-04-02T16:00:00Z'),
    totalAmount: 4518.47,
    currency: 'USD',
    supplier: { id: supplierId, displayName: 'Northwind Cloud Hosting' },
    reference: 'ACH-2025-04-02-00471',
    paymentMethod: 'ACH',
    account: { id: requiredEnv('APIDECK_PAYMENT_ACCOUNT_ID') },
    type: 'accounts_payable',
    status: 'authorised',
    reconciled: false,
    allocations: [{ id: billId, type: 'bill', amount: 4518.47 }]
  }
})
```

Store the returned payment ID before acknowledging your application's payment event:

```ts
const paymentId = createBillPaymentResponse?.data?.id
if (!paymentId) throw new Error('Payment result needs investigation before retrying')
```

An allocation links the payment to the bill. Confirm the connector's allocation behavior and supported balance fields instead of assuming that a successful response means the bill is fully paid. For partial payments, credit memos and overpayments, see [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid).

### Check the period is open

Many ledgers reject bills dated into a locked or closed period. [Fiscal Periods](/apis/accounting/reference/fiscal-periods) (beta) returns each period's `status` and `locked_through`; check the bill date against them before ingesting, and route bills for an earlier period to review.

## Verify the result

After bill creation, read it using the same consumer and connector. Repeat this read after recording a payment when the connector supports it:

```ts
const { getBillResponse } = await apideck.accounting.bills.get({
  serviceId: 'quickbooks',
  id: billId
})
const savedBill = getBillResponse?.data
if (!savedBill?.id) throw new Error('Could not verify the bill')
console.log({
  id: savedBill.id,
  supplierId: savedBill.supplier?.id,
  currency: savedBill.currency,
  total: savedBill.total,
  balance: savedBill.balance
})
```

For the example's fully allocated payment, the verification summary could look like this (illustrative values, not a live API response):

```json
{
  "id": "your-returned-bill-id",
  "supplierId": "your-resolved-supplier-id",
  "currency": "USD",
  "total": 4518.47,
  "balance": 0
}
```

Verify the returned supplier, currency and total against the approved invoice. Where supported, the outstanding balance should reflect recorded allocations; a partial payment leaves an outstanding amount. A missing field is not a zero balance.

Read the [bill payment](/apis/accounting/reference/bill-payments#billPaymentsOne) using its returned ID and verify its allocation points to the intended bill. Confirm the entries in the customer's ledger as well. Open [request logs](/logs) to inspect the request, normalized response and downstream details when the result differs from what you expected.

## What actually bites people

| Symptom                                  | What to inspect                                                                            | Next action                                                                               |
| ---------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| Authentication or connection failure     | App, consumer and connector IDs; Vault authorization and required settings                 | Correct the context or reconnect in Vault, then retry a read                              |
| Unsupported operation or missing field   | Operation and field coverage for the selected connector                                    | Remove unsupported assumptions; stop the write if a required capability is missing        |
| Bill validation fails                    | Supplier and account references, currency, dates, tax fields, dimensions and posting state | Inspect downstream error details in logs and fix the payload                              |
| Bill exists but payment is not allocated | Returned bill ID, payment allocation, account and company context                          | Inspect the payment and ledger; do not create a second payment just to retry verification |
| Create request times out                 | The write may have succeeded downstream                                                    | Reconcile with logs and the ledger before retrying; do not assume the write failed        |
| Duplicate supplier, bill or payment      | Concurrent jobs, repeated events and missing stored IDs                                    | Add a durable operation record and serialize writes for the same business event           |

Use the [error reference](/errors) for response details. Backoff behavior must account for the operation: a transient error on a read and an uncertain result from a financial write need different recovery paths.

## Connector-specific behavior

Use [operation coverage](/apis/accounting/coverage?resources=suppliers,ledger-accounts,bills,bill-payments,attachments) and [field coverage](/apis/accounting/coverage?resources=suppliers,ledger-accounts,bills,bill-payments&mode=fields) as the maintained source. Check filters, account identifiers, bill states, tax treatment and dimensions as well as create/read support. Confirm an alternative payment or journal-entry approach separately; those resources are not automatically interchangeable with bill payments.

### NetSuite

Check the customer's subsidiary and dimension requirements in the [NetSuite connector guide](/apis/accounting/netsuite) before writing. Keep IDs scoped to the selected company and subsidiary.

### Exact Online

Check supplier and bill write operations for the exact regional connector the customer uses. Where suppliers are read-only, resolve an existing supplier rather than offering vendor creation in your application.

### Acumatica, MYOB Acumatica, Visma Netvisor, Pennylane, Procountor

Verify bill-payment support independently of bill creation. If the dedicated payment operation is unavailable, define a supported reconciliation path before promising automatic settlement in the ledger.

### Microsoft Dynamics 365 Business Central

Follow the [connector setup](/apis/accounting/microsoft-dynamics-365-business-central) and verify company, account and allocation requirements in a test company before recording payments.

## Production checklist

- Keep credentials server-side and test that one tenant cannot use another tenant's consumer or record IDs.
- Persist supplier, bill and payment IDs with the originating business event. Enforce duplicate prevention in durable storage, including concurrent jobs.
- Reconcile uncertain writes before retrying. Do not assume a reference or bill number is an idempotency key.
- Paginate list reads and verify filter support before implementing incremental sync.
- Exercise disconnected accounts, expired authorization, validation failures, rate limits, partial payments and interrupted writes.
- If using [webhooks](/guides/webhooks), verify supported events, validate signatures and deduplicate deliveries.
- Recheck connector coverage and permissions when expanding to another accounting platform.

## Continue with an agent

Use the [build prompt](/accounts-payable-build-prompt.txt) for a new integration or the [review prompt](/accounts-payable-review-prompt.txt) to audit an existing one. Include your connector and SDK version. Both prompts require coverage checks, tenant isolation, duplicate prevention and verification of ledger results. [Set up agent tools](/building-with-llms) or read this [guide as Markdown](/guides/accounts-payable-automation.md).

## Work a bill through partial failure

Use a test-company bill for 240.00 in the company's currency, with no tax in this illustrative scenario. Confirm the supplier and expense account exist before sending it. The checkpoints below are application state, not an API response schema.

| Checkpoint | Evidence to retain | Next action |
| --- | --- | --- |
| Approved locally | Tenant, consumer, connector, source bill ID and approved amount | Create the bill only after resolving its supplier and line references. |
| Bill created | Returned bill ID and a successful read-back of the supplier, currency and total | Save the downstream ID before attempting an attachment or payment. |
| Attachment fails | Bill ID and attachment error | Retry only the supported attachment operation; do not recreate the bill. |
| Payment of 100.00 settles | External settlement reference and bill ID | Record a supported bill payment once, then read back the bill and payment. |
| Reconciled | Payment allocation and remaining balance | Expect 140.00 only if there are no taxes, credits, currency differences or other adjustments. Investigate discrepancies instead of forcing a local balance. |

Key your export job by tenant, connection and source bill ID. Keep a durable record of the returned bill and payment IDs. A local job key does not make a downstream write idempotent: after a timeout, inspect Logs and reconcile an existing result before creating another bill. If the provider cannot conclusively identify the result, hold the job for review.

Accept this workflow only after testing a rejected account reference, a failed attachment after bill creation, and a lost response after a successful write. None of these should create a second bill or record a payment twice. Use [child requests](/logs#follow-parent-and-child-requests) to distinguish the failed step.

## Frequently asked questions

### Does creating a bill payment transfer money to the supplier?

No. A bill-payment record documents a completed payment in the accounting system; it does not execute a bank transfer. Your application or payment provider handles settlement. Record and verify the accounting payment only after you know the settlement outcome.

### Do all accounting connectors support the complete payables workflow?

No. Check suppliers, accounts, bills, bill payments, and any attachment operations separately. Supported fields and account references also vary. Use the [payables coverage matrix](/apis/accounting/coverage?resources=suppliers,ledger-accounts,bills,bill-payments,attachments) before committing to the workflow.

### Should I recreate the bill when an attachment upload fails?

No. Keep the successfully created bill ID and retry only the supported attachment step after diagnosing the failure. Recreating the bill can duplicate the supplier liability. Follow [Work a bill through partial failure](#work-a-bill-through-partial-failure).

### How do I avoid duplicate bills after a timeout?

Persist the source business event and any returned bill ID, then investigate the uncertain request before retrying. A local job key or bill reference is not a guaranteed downstream idempotency key. Use [parent and child request logs](/logs#follow-parent-and-child-requests) and read-back evidence; hold the job for review if the result remains uncertain.

## Next steps

- [Mark Invoices and Bills as Paid](/guides/mark-invoices-as-paid) for the payment-allocation pattern in more depth
- [Handling Bills and Expenses](/guides/expenses-bills) for the broader Bills vs Expenses decision
- [Accounting API reference](/apis/accounting/reference) for the full unified schema
- [`GET /accounting/aged-creditors`](/apis/accounting/reference#operation/agedCreditorsOne) for AP aging reports without recomputing buckets from raw bills

## Related reading

From the Apideck blog.

- [Accounting & ERP Partnership Guide](https://www.apideck.com/blog/accounting-and-erp-partnership-guide): Understand the partnership and production-access requirements to investigate when planning an accounting connector launch.
- [Apideck and Dolfin AI: the connectivity layer for embedded finance agents](https://www.apideck.com/blog/apideck-dolfin-ai-embedded-finance-agents): See how Dolfin approaches embedded accounts payable and receivable agents with connected accounting systems.
