A command-line interface for the QuickBooks Online API. Query entities, run reports, create invoices — all from your terminal. The QuickBooks CLI that developers actually want to use.
Built for QuickBooks automation — giving AI agents (like OpenClaw) and scripts clean, direct QBO command line access without dealing with unmaintained MCP servers or raw API calls every time. Also works great as a standalone QuickBooks API Python tool for developers who live in the terminal.
- OAuth 2.0 authentication with local callback server or manual mode
- Query entities using QBO's SQL-like syntax with automatic pagination
- Local text search over query results (useful for fields not queryable server-side)
- CRUD + void operations on any QBO entity (Customer, Invoice, Bill, etc.)
- Financial reports — P&L, Balance Sheet, Cash Flow, and more
- Raw API access for anything the CLI doesn't cover
- Auto token refresh — access tokens refresh transparently
- Flexible date input —
YYYY-MM-DD,DD.MM.YYYY,DD/MM/YYYYwith-b/-eshorthands - TSV and JSON output — pipe to
jq,awk, spreadsheets - Flexible output flags — use global
-f/--formator subcommand--format/-o - Named profiles (
prod/dev) for credential isolation across environments - Sandbox support for development and testing
- File-locked token storage — safe for concurrent use
uv tool install qbo-cliOr with pip: pip install qbo-cli
Requires Python 3.9+ on macOS or Linux (uses fcntl for file locking).
Go to developer.intuit.com, create an app, and note your Client ID and Client Secret.
Add a Redirect URI in your app's settings. For production apps, Intuit requires HTTPS with a real domain (e.g., https://yourapp.example.com/callback). For development, https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl works. Set QBO_REDIRECT_URI to match.
Tip: If you're running on a headless server, use
qbo auth init --manual— the redirect doesn't need to resolve. You'll just copy the URL from your browser's address bar after authorization.
Quickest: Interactive setup
qbo auth setup # production profile (default)
qbo --profile dev auth setup # development/sandbox profileThis prompts for your Client ID, Client Secret, and Redirect URI, then saves to ~/.qbo/config.json.
Intuit provides separate Development and Production keys per app. Use --profile dev for sandbox keys.
Option A: Environment variables (for CI/scripts)
export QBO_CLIENT_ID="your-client-id"
export QBO_CLIENT_SECRET="your-client-secret"
export QBO_PROFILE="dev" # optional: select profileOption B: Config file (~/.qbo/config.json)
{
"prod": {
"client_id": "your-production-client-id",
"client_secret": "your-production-client-secret"
},
"dev": {
"client_id": "your-development-client-id",
"client_secret": "your-development-client-secret",
"sandbox": true
}
}Environment variables take precedence over the config file.
qbo auth init # production
qbo --sandbox auth init # development/sandboxThis opens an OAuth flow — authorize in your browser, and tokens are saved per-profile (~/.qbo/tokens.prod.json, ~/.qbo/tokens.dev.json, chmod 600).
On headless servers, use manual mode:
qbo auth init --manualqbo auth status# All customers
qbo query "SELECT * FROM Customer"
# Recent invoices
qbo query "SELECT * FROM Invoice WHERE TxnDate > '2025-01-01'"
# Unpaid invoices
qbo query "SELECT * FROM Invoice WHERE Balance > '0'"
# Vendors with email
qbo query "SELECT DisplayName, PrimaryEmailAddr FROM Vendor"
# Count items
qbo query "SELECT COUNT(*) FROM Item"
# TSV output (great for spreadsheets)
qbo query "SELECT DisplayName, Balance FROM Customer WHERE Balance > '0'" -f tsvQueries automatically paginate through all results (up to 100 pages × 1000 rows).
Some QBO fields are hard or impossible to filter server-side. Use search to run a normal QBO query, then filter returned rows locally by substring match against each row's JSON.
# Find invoices containing text in any field (including nested line descriptions/memos)
qbo search "SELECT * FROM Invoice WHERE TxnDate >= '2025-01-01'" "consulting fee"
# Case-sensitive search
qbo search "SELECT * FROM Invoice" "Owner Memo" --case-sensitive
# JSON output
qbo search "SELECT * FROM Invoice" "memo" --format jsonqbo get Customer 5
qbo get Invoice 1042echo '{
"DisplayName": "John Smith",
"PrimaryEmailAddr": {"Address": "john@example.com"}
}' | qbo create Customer
echo '{
"CustomerRef": {"value": "5"},
"Line": [{
"Amount": 150.00,
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": {"ItemRef": {"value": "1"}}
}]
}' | qbo create Invoice# Fetch, modify, and update
qbo get Customer 5 | jq '.Customer.CompanyName = "New Name"' | qbo update Customerqbo delete Invoice 1042The CLI fetches the entity first (to get the required SyncToken), then deletes it.
qbo void Invoice 1042
qbo void SalesReceipt 55Void keeps the transaction on the books as $0 (preserving audit trail), unlike delete which removes it entirely. The CLI fetches the entity first, then posts with ?operation=void.
# Profit and Loss for a date range
qbo report ProfitAndLoss --start-date 2025-01-01 --end-date 2025-12-31
# Balance Sheet as of today
qbo report BalanceSheet
# Using date macros
qbo report ProfitAndLoss --date-macro "Last Month"
qbo report ProfitAndLoss --date-macro "This Year"
# With extra parameters
qbo report ProfitAndLoss --start-date 2025-01-01 --end-date 2025-12-31 accounting_method=CashAvailable reports: ProfitAndLoss, BalanceSheet, CashFlow, CustomerIncome, AgedReceivables, AgedPayables, GeneralLedger, TrialBalance, and more.
# GET request
qbo raw GET "query?query=SELECT * FROM CompanyInfo"
# POST with body
echo '{"TrackQtyOnHand": true}' | qbo raw POST "item"Hierarchical GL reports for any account and customer, with auto-discovered sub-account trees.
# Explore your chart of accounts
qbo gl-report --list-accounts
# Drill into a specific account's sub-accounts
qbo gl-report -a 125 --list-accounts
# Account tree as JSON
qbo gl-report -a 125 --list-accounts --format json
# Generate a report (text by default)
qbo gl-report -c "John Smith" -a 125
# Human-readable text with currency prefix
qbo gl-report -c "John Smith" -a 125 --currency USD
# Custom date range
qbo gl-report -c "John Smith" -a "Revenue" --start 2025-01-01 --end 2025-12-31
# Short flags and flexible date formats
qbo gl-report -c "John Smith" -a 125 -b 01.01.2025 -e 31.12.2025
# Structured JSON output
qbo gl-report -a 125 --format json
# Dates default to: first transaction → today
qbo gl-report -c "John Smith" -a 125gl-report supports text, json, txns, and expanded. tsv is not available for this command.
# Text output (default, human-readable table)
qbo query "SELECT * FROM Customer"
# JSON
qbo query "SELECT * FROM Customer" -f json
qbo query "SELECT * FROM Customer" --format json
# TSV (tab-separated, for spreadsheets/awk)
qbo query "SELECT * FROM Customer" -f tsv
# Pipe to jq
qbo query "SELECT * FROM Customer" -f json | jq '.[].DisplayName'# Use dev profile (sandbox keys + sandbox API endpoint)
qbo --sandbox query "SELECT * FROM Customer"
qbo --profile dev query "SELECT * FROM Customer" # equivalent
# Switch profile via env var
export QBO_PROFILE=dev
qbo query "SELECT * FROM Customer"| Setting | Env Variable | Config Key | Default |
|---|---|---|---|
| Profile | QBO_PROFILE |
— | prod |
| Client ID | QBO_CLIENT_ID |
client_id |
— |
| Client Secret | QBO_CLIENT_SECRET |
client_secret |
— |
| Redirect URI | QBO_REDIRECT_URI |
redirect_uri |
http://localhost:8844/callback |
| Realm ID | QBO_REALM_ID |
realm_id |
From auth flow |
| Sandbox mode | — | sandbox |
false |
Config file: ~/.qbo/config.json (profiled format, see config.json.example)
Token storage: ~/.qbo/tokens.{profile}.json (per-profile, created automatically, chmod 600)
- Access tokens expire every 60 minutes. The CLI refreshes them automatically before each request.
- Refresh tokens are valid for 100 days. Each refresh extends the 100-day window (rolling expiry).
- If you don't use the CLI for 100+ days, the refresh token expires and you need to re-authorize with
qbo auth init. - The CLI warns you when the refresh token has fewer than 14 days remaining.
- Token refresh uses file locking — safe to run concurrent
qbocommands.
Force a manual refresh:
qbo auth refreshLint — runs on every push and PR to main:
ruff check(errors, warnings, import sorting)ruff format --check(code style)
Tests — runs on every push and PR to main (Python 3.9 + 3.12).
Publish — auto-publishes to PyPI when you create a GitHub Release:
- Bump version in
pyproject.toml(single source of truth —qbo_cli/__init__.pyreads it viaimportlib.metadata) - Commit and push
- Create a GitHub Release (tag
vX.Y.Z) uv build+ trusted publishing → PyPI
First-time setup: Add
qbo-clias a trusted publisher on PyPI → Your projects → Publishing → add GitHub publisher:alexph-dev/qbo-cli, workflowpublish.yml, environmentpypi.
Contributions welcome. Please open an issue first to discuss what you'd like to change.
git clone https://github.com/alexph-dev/qbo-cli.git
cd qbo-cli
uv sync --extra test
uv run pytest # run tests
uv run ruff check qbo_cli/ # lint before committingMIT — see LICENSE.