Last updated 2026-04-25
QuickBooks integration
/finance/qb connects each business entity to its own QuickBooks Online realm. Pulls invoices, bills, and payments since 90 days ago into the entity's monthly snapshots. Pushes closed sales to QB as invoices.
Setup (one-time per company)
- Register an Intuit app at https://developer.intuit.com:
- Create a new app under "QuickBooks Online and Payments"
- Add redirect URI:
https://otiumwork.com/finance/qb/callback- Get Client ID + Client Secret (production keys, not sandbox unless testing) - Save credentials at
/admin/settings→ QuickBooks section. Pick environment (production / sandbox). - Per entity, click Connect to QuickBooks at
/finance/qb. You'll be redirected to Intuit, pick the QB company (realm) for that entity, authorize, and land back here.
Each entity → one QB realm. SoftInWay India connects to its India QB; Switzerland connects to its CH QB; etc.
What pulls in (sync direction: QB → OtiumWork)
When you click Sync now (or the cron runs):
| QB entity | Aggregates into snapshot field |
|---|---|
| Invoice | ar_balance (open balance) + revenue (paid portion, by month) |
| Bill | ap_balance (open balance) + expenses |
| Payment | (covered by invoice paid logic) |
QB is treated as authoritative for these fields — sync overwrites whatever was there with QB's numbers. Manual entries in those buckets get replaced.
The sync covers the last 90 days. For older history, edit the snapshot directly — it won't be touched.
What pushes out (sync direction: OtiumWork → QB)
A sale at status closed can be pushed via the Push as QB invoice button on the sale detail page:
- Resolves the OtiumWork client to a QB customer (creates one if no name match)
- Creates a QB invoice with
Description = sale.name,Amount = sale.amount_usd,TxnDate = signed_date - Stamps
PrivateNote = OW-Sale-{id}for idempotency — re-pushing finds the existing invoice instead of creating a duplicate
Payments coming back into that invoice get pulled on the next sync.
Per-entity model
- Each entity has its own QB connection (realm + refresh token)
- Disconnecting one entity doesn't affect others
- Push from a sale uses the salesperson's sale's entity to pick which QB realm to push into
Sync log
Every action (created / updated / skipped / error) lands in quickbooks_sync_log and renders in the dashboard table. Useful for debugging.
Cron
POST https://otiumwork.com/api/cron/qb-sync-all
Header: X-OtiumWork-Cron-Token: <CRON_TOKEN env>
Hourly is plenty. Iterates every active connection across companies and entities.
What's not in V1
- Webhooks — QB has them, but they need a public endpoint with signature verification + replay protection. Manual sync + cron covers 95% of the use case.
- Customer/Vendor entity ledger — we don't store QB customers / vendors as OtiumWork rows. Names are matched by string.
- Tax handling — QB has a complex tax engine; we don't push line-item taxes. Pushed invoices come over tax-free; admin sets tax in QB.
- Multi-currency invoices — V1 amounts go up as USD only. QB handles the realm currency at its end.
- Bidirectional payment sync — we pull payments via the invoice query, but don't push OtiumWork sale_payments back to QB as separate Payment rows.
- Bill push — we don't push expense receipts back to QB (mailbox scanner only updates monthly snapshots, not QB bills).
Token lifecycle
- Access token: 1 hour. Auto-refreshed when stale.
- Refresh token: 100 days. Rotated on every refresh.
- If a connection sits unused for 100 days, the refresh token expires; admin must reconnect.
See something wrong or outdated in this article? Report it →