Skip to main content

QuickBooks Online

With QuickBooks Online, QuickBooks computes the tax and QuickBooks emails the invoice. Vodia Billing supplies the lines.

QuickBooks is treated as an external tax authority. That has consequences throughout, and they are all deliberate.

Vodia Billing 14

Setup

1. Register the app

In the Intuit developer portal, create an app with the Accounting scope.

Redirect URI:

https://your-domain/api/quickbooks/callback

Copy the client ID and client secret. Note that sandbox and production credentials are different; use production credentials for a real install.

2. Enter the credentials

Accounting → App, choose QuickBooks, paste the client ID and secret. Stored encrypted under ENCRYPTION_KEY.

3. Connect the company

Accounting → Connect. Authorise the QuickBooks company you bill from. The company name appears on the connection.

4. Discover

Discover pulls the customer list, chart of accounts and QuickBooks items.

5. Map items and accounts

QuickBooks invoice lines reference items, not bare account codes. Map:

  • Usage
  • Recurring
  • Goods
  • Discount

Vodia Billing can create the items for you from the mapping screen if they do not exist.

6. Map customers

Map each billing tenant to a QuickBooks customer. Auto-bind proposes matches; confirm each one.

7. Do not apply a tax profile

A QuickBooks tenant does not need a tax profile in Vodia Billing, because QuickBooks computes the tax. Set the tenant's tax authority to the accounting package.

Totals: QuickBooks wins

Because QuickBooks computes the tax, it owns the invoice total. Vodia Billing's own total will frequently differ.

For example, the same invoice may total 199.43 in Vodia Billing and 197.85 in QuickBooks. That is not an error. QuickBooks applied its own tax rules to the lines it was given, and its figure is the one the customer sees and pays.

The QuickBooks figure is used as the authority for payment tracking, so reminders, overdue detection and paid/unpaid state all follow QuickBooks rather than the internally computed total. The divergence detector accounts for this and does not alert on an expected difference.

Tax is still computed and stored

Vodia Billing computes and stores a tax figure on QuickBooks invoices as well. The renderer, payment tracking and export all correctly use the QuickBooks figure instead, so this stored value is unused rather than wrong. It is noted here so it is not mistaken for a live discrepancy if you look directly at the database.

Discounts

Discounts travel to QuickBooks as their own negative line, so QuickBooks computes tax on the discounted base rather than the gross. This is the correct treatment and is verified.

Sending

QuickBooks emails the invoice. Vodia Billing marks the invoice as externally delivered and does not send its own copy, so the customer receives exactly one invoice.

Reminders follow the same route: the invoice is treated as externally delivered throughout its lifecycle.

Sandbox cannot send email

The QuickBooks sandbox reports its email state honestly as sandbox and does not send mail. Email delivery can only be verified against a production QuickBooks company.

Confirm that your first pushed invoice actually reaches the customer before relying on this route, and check the invoice's own send status in QuickBooks rather than assuming a successful push means a delivered email.

Credits

Credits cannot be raised in Vodia Billing for a QuickBooks tenant. Raise the credit note in QuickBooks instead.

The credit control is greyed out on such a tenant and the API refuses the request. This is intentional: a credit computed against a total Vodia Billing does not own would disagree with the accounting record.

Token refresh

QuickBooks refresh tokens rotate on use. Vodia Billing refreshes single-flight, coordinating through a database lock so that two simultaneous requests cannot invalidate each other's tokens.

If a connection does lapse, the accounting_disconnected and accounting_expiring detectors raise alerts. Re-authorise from Accounting → Connect; the customer and account mappings survive.

Push behaviour

A push is refused locally when the invoice is not finalized, has already been pushed, has a sibling already pushed for the same period, has no customer mapping, or has no due date.

Push state is per invoice, so a bill run reports pushed, blocked and failed separately and a retry touches only what failed.

Worked example

What arrives in QuickBooks

The invoice from Worked Example, pushed to QuickBooks Online. Note that no tax components are sent:

Invoice 1051             Customer: Acme Corp
Date 2026-09-01 Due 2026-09-15

Line Item Qty Rate Amount
US National Call Usage 1240.00 0.012 14.88
United Kingdom Call Usage 86.00 0.035 3.01
UK Mobile Call Usage 12.00 0.14 1.68
US Toll Free Call Usage 318.00 0.00 0.00
Extensions Recurring 42 12.00 504.00
Hunt groups Recurring 3 5.00 15.00
DID numbers Recurring 18 1.50 27.00
Managed support plan Recurring 1 150.00 150.00
Desk handset Hardware 1 89.00 89.00

Subtotal 804.57
Sales Tax (QuickBooks computed) 77.03
TOTAL 881.60

Vodia Billing said 887.29. QuickBooks says 881.60.

That is expected and correct. QuickBooks applied its own tax rules to the lines it was given, and it is the tax authority for this tenant. Its figure is what the customer sees, what the customer pays, and what payment tracking, reminders and overdue detection all follow.

The divergence detector accounts for this and does not alert on an expected difference.

With a discount

The discount travels as its own negative line, so QuickBooks taxes the discounted base rather than the gross:

  Line                     Item              Qty      Rate      Amount
... usage lines ... 19.57
Usage discount 10% Discount 1 -1.96 -1.96
... recurring lines ... 696.00
Desk handset Hardware 1 89.00 89.00

Subtotal 802.61
Sales Tax (QuickBooks computed) 76.85
TOTAL 879.46

Had the discount been applied only to the Vodia Billing total and not sent as a line, QuickBooks would have taxed the full 804.57.

A credit attempt

POST /api/credits  { tenant: "acme-qbo.example.com", amount: 5000 }
409 Conflict — tenant tax is owned by QuickBooks Online.
Raise the credit note in QuickBooks.

Greyed out in the interface and refused at the API. Raise it in QuickBooks, which owns the total.

Email state

Production:

INV-00151  ->  QuickBooks invoice 1051
email_state: sent 2026-09-01 03:16 accounts@customer-example.com
Vodia Billing did not send its own copy

Sandbox:

INV-00151  ->  QuickBooks invoice 1051
email_state: sandbox (QuickBooks sandbox cannot send mail)

The sandbox reports its state honestly rather than claiming success. On a production company, confirm your first pushed invoice actually reached the customer and check the send status in QuickBooks itself.

A lapsed connection

accounting_disconnected   Sandbox Company US   refresh token rejected   high

Re-authorise under Accounting → Connect. Customer mappings, item mappings and account mappings all survive a re-authorisation.