QuickBooks Online connector

Sandbox-first OAuth service for qbo.fdtorres.com.

This small Next.js service handles QuickBooks Online OAuth, stores the latest tokens in a local JSON file, exposes a health endpoint, and now provides a native-first bookkeeping v1: standard QuickBooks objects for vendors, expenses, and reports, with journal entries reserved for exception handling only.

Status

Ready for OAuth

Environmentsandbox
API base URLhttps://sandbox-quickbooks.api.intuit.com
Current realm9341455204820813
Stored connections1
Token store path/app/data/qbo-tokens.json

Expected env vars

  • QBO_CLIENT_ID
  • QBO_CLIENT_SECRET
  • QBO_ENV
  • QBO_REDIRECT_URI
  • QBO_SCOPES
  • QBO_BASE_URL

Connect flow

  1. Set the env vars in .env.local.
  2. Make sure Intuit has the exact redirect URI configured for /api/qbo/callback.
  3. Visit /api/qbo/connect and authorize a sandbox company.
  4. After callback, call /api/qbo/company-info to verify the stored token works.
  5. Use the native-first read endpoints below to inspect vendors, core lists, and financial reports before enabling guarded writes.
GET /api/qbo/connect
GET /api/qbo/callback
GET /api/qbo/health
GET /api/qbo/company-info
GET /api/qbo/customers?limit=25&query=Acme
GET /api/qbo/invoices?limit=25
GET /api/qbo/accounts?limit=25
GET /api/qbo/accounts/resolve?limit=5&query=Office&classification=Expense
GET /api/qbo/items?limit=25&query=Widget
GET /api/qbo/mappings
GET /api/qbo/payments?limit=25
GET /api/qbo/payment-accounts/resolve?limit=5&query=Amex
GET /api/qbo/vendors?limit=25&query=Acme
GET /api/qbo/vendors/resolve?limit=5&query=Staples
GET /api/qbo/reports/profit-and-loss?startDate=2026-01-01&endDate=2026-03-31
GET /api/qbo/reports/balance-sheet?endDate=2026-03-31
GET /api/qbo/reports/transaction-list?startDate=2026-03-01&endDate=2026-03-31
POST /api/qbo/categorize/dry-run

Bookkeeping v1

Policy: prefer native QuickBooks objects for normal bookkeeping. Use vendors and expenses first. Journal entries are available only for exceptions and adjustments.

  • POST /api/qbo/vendors requires allowWrite=true.
  • POST /api/qbo/expenses creates native QuickBooks Purchase transactions with vendor, payment account, txn date, memo, and expense lines, either from explicit line items or from a reviewed categorization result.
  • GET /api/qbo/accounts/resolve returns compact account candidates ranked by name and fully qualified name for posting-account selection.
  • GET /api/qbo/payment-accounts/resolve narrows account lookup to likely payment accounts for paymentAccountId.
  • POST /api/qbo/journal-entries requires both allowWrite=true and allowJournalEntry=true.
  • GET /api/qbo/mappings exposes the current local mapping config from config/qbo-mappings.json.
  • POST /api/qbo/categorize/dry-run evaluates disconnected account transactions against those rules and returns a proposed treatment without writing anything to QuickBooks.
  • Mapping-driven expense posts still require a separate POST /api/qbo/expenses call with allowWrite=true; dry-run alone never posts.
  • GET /api/qbo/vendors/resolve returns compact vendor matches for explicit operator selection before retrying a mapping-driven expense create.
  • If mapping-driven expense create receives paymentAccountName without paymentAccountId, the route returns a structured payment-account-resolution response with candidate payment accounts. It never auto-selects or auto-writes.
  • All write routes return clear 400s for invalid JSON or missing required fields.

Mapping dry-run

Keep categorization rules in config/qbo-mappings.json. The format is plain JSON so operators can review and edit it without touching route code.

  • Rules can match on source account key or name, vendor hints, memo text, and description text.
  • Each rule proposes a deterministic treatment: expense, income, transfer, ignore, or unmapped when nothing matches.
  • Dry-run responses include the matched rule, proposed QBO account, optional class or location defaults, and a plain-English confidence summary.
  • The expense route can reuse that reviewed result to derive a one-line native expense payload, but the caller still has to provide the payment account and a resolved QBO vendor ID.
  • If vendorId is missing but vendorName is present, the expense route returns a structured vendor-resolution response with a search query plus top candidate vendors. It never auto-selects or auto-creates.
  • If paymentAccountId is missing but paymentAccountName is present in mapping-driven mode, the expense route returns a structured payment-account-resolution response with a search query plus top candidate payment accounts. It never auto-selects.
POST /api/qbo/categorize/dry-run
{
  "sourceAccount": { "key": "amex-blue", "name": "Amex Blue Business Card" },
  "date": "2026-04-15",
  "amount": 72.45,
  "direction": "expense",
  "description": "Staples store purchase",
  "payee": "Staples",
  "memo": "office supplies order"
}
POST /api/qbo/expenses
{
  "allowWrite": true,
  "paymentAccountName": "Amex Blue Business Card",
  "transaction": {
    "sourceAccount": { "key": "amex-blue", "name": "Amex Blue Business Card" },
    "date": "2026-04-15",
    "amount": 72.45,
    "direction": "expense",
    "description": "Staples store purchase",
    "payee": "Staples",
    "memo": "office supplies order"
  },
  "categorization": {
    "proposedTreatment": "expense",
    "proposedQboAccountId": "84",
    "proposedVendorHint": "staples",
    "proposedClassId": "200",
    "proposedLocationId": "10"
  },
  "vendorName": "Staples"
}
GET /api/qbo/payment-accounts/resolve?query=Amex&limit=5

{
  "ok": true,
  "paymentAccounts": [
    {
      "id": "35",
      "name": "Amex Blue Business Card",
      "fullyQualifiedName": "Credit Cards:Amex Blue Business Card",
      "accountType": "Credit Card",
      "accountSubType": "CreditCard",
      "classification": "Asset",
      "active": true
    }
  ]
}
GET /api/qbo/vendors/resolve?query=Staples&limit=5

{
  "ok": true,
  "vendors": [
    {
      "id": "58",
      "displayName": "Staples",
      "companyName": "Staples Inc.",
      "primaryEmail": "ap@staples.test",
      "active": true
    }
  ]
}
POST /api/qbo/vendors
{
  "allowWrite": true,
  "displayName": "Staples",
  "companyName": "Staples Inc.",
  "primaryEmail": "ap@staples.test"
}