# Accounts Receivable Automation

By the end of this page you'll have created a customer, pushed an invoice with its line items coded to revenue accounts, settled it with an allocated payment, and read the balance back to confirm, inside a customer's QuickBooks, Xero, or NetSuite. One integration against the unified [Accounting API](/apis/accounting/reference), three calls, about ten minutes.

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

Payment allocations are what let the downstream system flip invoice status to paid using its own rules, [Vault](/guides/vault) handles auth and token refresh, and the `accounting.invoice.updated` and `accounting.payment.created` webhooks keep reconciliation in sync without polling.

## Resource mapping

| Connector | Invoice maps to |
| --- | --- |
| QuickBooks | `Invoice` |
| Xero | `Invoice` (type `ACCREC`) |
| NetSuite | `Invoice` (transaction type) |
| Sage Intacct | `AR Invoice` |
| Exact Online | `Sales Invoice` |
| Stripe | `Invoice` (with auto-collected `PaymentIntent`) |
| Odoo | `account.move` (type `out_invoice`) |
| FreshBooks | `Invoice` |
| Zoho Books | `Invoice` |
| Workday | `Customer Invoice` |

## Walkthrough

The flow is the same for every connector that supports the full path: resolve the customer, create the invoice, then record a payment with an allocation back to that invoice.

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: 'cons_01H8X9Y2A3K4M5N6P7Q8R9S0T1'
})
```

### 1. Resolve or create the customer

Before pushing an invoice, you need a `customer.id` that exists in the downstream system. Search by email or display name first to avoid creating duplicates, then fall back to creating a new customer if no match is found.

```json
{
  "display_name": "Beachside Surf Co.",
  "company_name": "Beachside Surf Co. LLC",
  "emails": [
    {
      "email": "ap@beachsidesurf.com",
      "type": "primary"
    }
  ],
  "currency": "USD",
  "addresses": [
    {
      "type": "billing",
      "line1": "221 Pacific Ave",
      "city": "Santa Cruz",
      "state": "CA",
      "postal_code": "95060",
      "country": "US"
    }
  ],
  "phone_numbers": [
    {
      "number": "+1-831-555-0144",
      "type": "primary"
    }
  ],
  "payment_method": "credit_card"
}
```

Send this to [`POST /accounting/customers`](/apis/accounting/reference#operation/customersAdd) with the standard headers:

```http
POST /accounting/customers HTTP/1.1
Host: unify.apideck.com
Authorization: Bearer ${APIDECK_API_KEY}
x-apideck-app-id: ${APIDECK_APP_ID}
x-apideck-consumer-id: cons_01H8X9Y2A3K4M5N6P7Q8R9S0T1
x-apideck-service-id: quickbooks
Content-Type: application/json
```

Or through the SDK, looking the customer up first so you do not create duplicates:

```ts
const { getCustomersResponse } = await apideck.accounting.customers.list({
  serviceId: 'quickbooks',
  filter: { companyName: 'Beachside Surf Co. LLC' },
  limit: 1
})

const customerId =
  getCustomersResponse?.data?.[0]?.id ??
  (await apideck.accounting.customers.create({
    serviceId: 'quickbooks',
    customer: {
      displayName: 'Beachside Surf Co.',
      companyName: 'Beachside Surf Co. LLC',
      emails: [{ email: 'ap@beachsidesurf.com', type: 'primary' }],
      currency: 'USD'
    }
  })).createCustomerResponse?.data?.id

if (!customerId) throw new Error('Could not resolve a customer to invoice')
```

The response includes the connector-issued `id` you will reference on the invoice. On connectors where customers are read-only (see the table below), skip this step and look up the existing customer with [`GET /accounting/customers`](/apis/accounting/reference#operation/customersAll).

### 2. Push the invoice

Create the invoice against the resolved customer. The unified model expects line items with `description`, `quantity`, and `unit_price`. The downstream system will compute totals and tax based on the `tax_rate` you supply.

```json
{
  "number": "INV-2025-00184",
  "customer": {
    "id": "acct_01H8X9Y2A3K4M5N6P7Q8R9S0T1"
  },
  "invoice_date": "2025-03-04",
  "due_date": "2025-04-03",
  "currency": "USD",
  "reference": "PO-44219",
  "line_items": [
    {
      "type": "sales_item",
      "description": "Pro plan subscription, March 2025",
      "quantity": 1,
      "unit_price": 199.00,
      "total_amount": 199.00,
      "tax_rate": {
        "id": "TAX_NON"
      }
    },
    {
      "type": "sales_item",
      "description": "Onboarding hours",
      "quantity": 4,
      "unit_price": 150.00,
      "total_amount": 600.00,
      "tax_rate": {
        "id": "TAX_NON"
      }
    }
  ],
  "sub_total": 799.00,
  "total": 799.00,
  "status": "authorised",
  "payment_method": "credit_card"
}
```

Send this to [`POST /accounting/invoices`](/apis/accounting/reference#operation/invoicesAdd). Capture the returned invoice `id`. That value is what you allocate the payment to in the next step.

The same call through the SDK. Pull the chart of accounts and tax rates once and cache them, rather than per invoice:

```ts
const { createInvoiceResponse } = await apideck.accounting.invoices.create({
  serviceId: 'quickbooks',
  invoice: {
    number: 'INV-2025-00184',
    customer: { id: customerId },
    invoiceDate: new Date('2025-03-04'),
    dueDate: new Date('2025-04-03'),
    currency: 'USD',
    reference: 'PO-44219',
    lineItems: [
      {
        type: 'sales_item',
        description: 'Pro plan subscription, March 2025',
        quantity: 1,
        unitPrice: 199,
        totalAmount: 199,
        taxRate: { id: 'TAX_NON' }
      },
      {
        type: 'sales_item',
        description: 'Onboarding hours',
        quantity: 4,
        unitPrice: 150,
        totalAmount: 600,
        taxRate: { id: 'TAX_NON' }
      }
    ],
    subTotal: 799,
    total: 799,
    status: 'authorised'
  }
})

const invoiceId = createInvoiceResponse?.data?.id
```

Two field names differ from what you might expect: the invoice number is `number`, not `invoiceNumber`, and the invoice-level figure is `total`, not `totalAmount`. `totalAmount` exists only on line items. Sending the wrong one is accepted and then silently dropped.

QuickBooks does not carry `status` or `subTotal` in its supported field set for invoices — 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 NetSuite, so the same body stays portable even though this snippet is pinned to QuickBooks.

### 3. Record the payment with an allocation

In most accounting systems, invoice status is computed from outstanding balance, not set directly. Trying to `PATCH` the invoice with `status: "paid"` will fail or be ignored. Allocate a payment instead, and the connector flips the status as part of the post.

```json
{
  "transaction_date": "2025-03-12T15:04:00.000Z",
  "currency": "USD",
  "total_amount": 799.00,
  "customer": {
    "id": "acct_01H8X9Y2A3K4M5N6P7Q8R9S0T1"
  },
  "payment_method": "Credit Card",
  "payment_method_reference": "ch_3OxPq2KJ8mZ4T1Lk0aN9bV2X",
  "accounts_receivable_account_type": "Account",
  "accounts_receivable_account_id": "1200",
  "type": "accounts_receivable",
  "reconciled": true,
  "status": "authorised",
  "allocations": [
    {
      "id": "inv_01H8YBZ3C4D5E6F7G8H9J0K1L2",
      "type": "invoice",
      "amount": 799.00
    }
  ],
  "note": "Stripe charge ch_3OxPq2KJ8mZ4T1Lk0aN9bV2X"
}
```

Or through the SDK:

```ts
await apideck.accounting.payments.create({
  serviceId: 'quickbooks',
  payment: {
    transactionDate: new Date('2025-03-12T15:04:00.000Z'),
    totalAmount: 799,
    currency: 'USD',
    type: 'accounts_receivable',
    customer: { id: customerId },
    reconciled: true,
    status: 'authorised',
    allocations: [{ id: invoiceId, type: 'invoice', amount: 799 }]
  }
})
```

Send this to [`POST /accounting/payments`](/apis/accounting/reference#operation/paymentsAdd). The `allocations[].id` is the invoice ID returned in step 2, and `allocations[].amount` can be partial if you are recording a deposit. Omit `allocations` entirely and the cash posts against the customer while the invoice stays open, which is the single most common AR bug. For a deeper breakdown of the allocation pattern, see the companion guide on [marking invoices and bills as paid](/guides/mark-invoices-as-paid).

### 4. Reconcile state via webhooks

Subscribe to `accounting.invoice.updated` and `accounting.payment.created` so your AR ledger stays in sync without polling. Use the invoice `balance` and `status` fields on the updated event to confirm the connector applied the allocation as expected.

## What actually bites people

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

**QuickBooks Online.** Neither `status` nor `sub_total` is a supported invoice field, so `invoice.status` comes back empty and QuickBooks derives the subtotal itself. Drive your paid, part paid, and overdue states off `balance` against `total` instead. It is also the only one of the three that lets you filter invoices by `customerId`.

**Xero.** Invoices support only the `updatedSince` and `number` filters, so you cannot ask Xero for one customer's invoices. Sync the whole window and index by customer on your side. Xero payments also have no update operation: to correct one you delete it and post a new one.

**NetSuite.** On a OneWorld account every AR record needs subsidiary context. Resolve subsidiaries with `apideck.accounting.subsidiaries.list()`, which NetSuite supports, and note that the unified `subsidiary` object is **not** writable on NetSuite customers, invoices, or payments: `companyId` is the supported scope field. NetSuite invoice line items also carry no `description`, so the line is defined by the `item` you reference and free text silently vanishes.

## Connector-specific behavior

Coverage below reflects whether each connector supports creating the resource through Apideck. Read-only means the resource lists through `GET` but cannot be written, so you must reference an existing record from the downstream system. Verify any limited or partial coverage in the live coverage matrix before depending on it.

| Connector | Notes |
| --- | --- |
| Access Financials | Customers, invoices, and payments are all writable. Standard AR flow. |
| Acumatica | Customers, invoices, and payments are all writable. Standard AR flow. |
| Banqup | Customers and invoices are read-only and payments are not in coverage. Use this connector for ingesting AR data, not pushing it. |
| Campfire | Customers and invoices are writable. Payment writes are limited; verify allocations land before treating an invoice as settled. |
| Clear Books (UK) | Customers and invoices are read-only. Payments are not in coverage. Read-only AR sync only. |
| Digits | Customers are read-only. Invoices and payments are not in coverage. Reporting use only. |
| DualEntry | Customers, invoices, and payments are writable. Standard AR flow. |
| Exact Online | Customers are read-only. Look up the customer ID with `GET /accounting/customers` before invoicing. Invoices and payments are writable. |
| Exact Online (NL) | Same as Exact Online. Customers read-only, invoices and payments writable. |
| Exact Online (UK) | Customers are read-only. Invoices and payments are writable. |
| FreeAgent | Customers and invoices are writable. Payments are not in coverage; mark invoices as paid through the FreeAgent UI or by other means. |
| FreshBooks | Customers, invoices, and payments are all writable. Standard AR flow. |
| Intuit Enterprise Suite | Customers, invoices, and payments are all writable. Standard AR flow. |
| KashFlow | Customers and invoices are read-only. Payments are read-only. Use for reporting only. |
| Microsoft Dynamics 365 Business Central | Customers and invoices are writable. Payments are read-only and must be posted natively. |
| Moneybird | Customers and invoices are writable. Payment writes are limited; confirm allocations land before treating an invoice as settled. |
| MRI Software | Customers are read-only. Invoices and payments are not in coverage. |
| MYOB | Customers, invoices, and payments are all writable. Standard AR flow. |
| MYOB Acumatica | Customers, invoices, and payments are all writable. Standard AR flow. |
| NetSuite | Customers, invoices, and payments are all writable. Subsidiary is required on every record in OneWorld accounts. |
| Odoo | Customers, invoices, and payments are all writable. Invoices map to `account.move` with type `out_invoice`. |
| Pennylane | Customers and invoices are writable. Payments are not in coverage. |
| Procountor (FI) | Customers and invoices are writable. Payments are read-only. |
| QuickBooks Online | Customers, invoices, and payments are all writable. Standard AR flow. |
| Rillet | Customers, invoices, and payments are all writable. Standard AR flow. |
| Sage Business Cloud Accounting | Customers, invoices, and payments are all writable. Standard AR flow. |
| Sage Intacct | Customers, invoices, and payments are all writable. Dimensions and locations may be required depending on the entity setup. |
| Sage Intacct (REST) | The REST connector does not yet expose customers, invoices, or payments. Use the legacy Sage Intacct connector for AR automation. |
| Stripe | Customers and invoices are writable. Payments are read-only because Stripe collects them itself. Treat the Stripe-emitted payment as the source of truth. |
| Visma Netvisor | Customers, invoices, and payments are all writable. Standard AR flow. |
| Wave | Customers and invoices are writable. Payments are not in coverage. Apply payments inside Wave or via journal entries. |
| Workday | Customers, invoices, and payments are all writable. Standard AR flow. |
| Xero | Customers, invoices, and payments are all writable. Send invoices with `status: "AUTHORISED"` to make them payable. |
| Yuki | Customers and invoices are writable. Payments are not in coverage. |
| Zoho Books | Customers, invoices, and payments are all writable. Standard AR flow. |

### Stripe

Stripe is not a general ledger. Apideck exposes Stripe customers and invoices as writable so you can push billable items, but payment objects are read-only because Stripe charges the card itself and emits the resulting payment. For an AR automation product, the typical pattern is to create the invoice through Apideck, let Stripe collect, then mirror the resulting payment into your other accounting connector via [`POST /accounting/payments`](/apis/accounting/reference#operation/paymentsAdd).

### NetSuite

In a OneWorld account, every customer, invoice, and payment must reference a subsidiary. Resolve subsidiaries with [`GET /accounting/subsidiaries`](/apis/accounting/reference#operation/subsidiariesAll) and persist the chosen subsidiary against the Apideck consumer so you do not have to look it up per request.

### Exact Online

Customers are read-only across the Exact Online connectors (`exact-online`, `exact-online-nl`, `exact-online-uk`). Build a customer picker in your UI that calls [`GET /accounting/customers`](/apis/accounting/reference#operation/customersAll) and have the user select the matching record before you push an invoice.

### QuickBooks Online and Xero

Both flip invoice status to paid automatically when a payment with an `allocations[]` entry is posted. Do not attempt to update the invoice status directly. The connector will reject status writes against computed fields.

## Visit the demo

The Vertical SaaS demo runs this exact flow against a live connected ledger: it pushes invoices, records the payment, and reads the balance back. Nothing to set up.

## Next steps

- [Mark invoices and bills as paid](/guides/mark-invoices-as-paid) for the deeper allocation pattern, including partial payments and credit notes.
- [Handling bills and expenses](/guides/expenses-bills) for the accounts payable counterpart of this flow.
- [Accounting API reference](/apis/accounting/reference) for the full list of operations and fields.
