Upload receipts, auto-tag expenses from learned merchant memory, and close out the month in one command — the agent-friendly Zoho Expense CLI nobody else has built.
Zoho ships autoscan; this CLI ships everything that comes after — receipt dedup, learned merchant→category mapping, India GST splitting, and monthly report bundling. Built for AI agents that ingest invoices from an inbox on a cadence and need an idempotent, structured-output upload path. India-region by default; works against any Zoho datacenter by overriding the base URL.
Learn more at Zoho Expense.
Created by @amitav13 (Amitav Khandelwal).
The recommended path installs both the zoho-expense-pp-cli binary and the pp-zoho-expense agent skill (Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, and other agents supported by the upstream skills CLI) in one shot:
npx -y @mvanhorn/printing-press-library install zoho-expenseFor CLI only (no skill):
npx -y @mvanhorn/printing-press-library install zoho-expense --cli-onlyFor skill only — installs the skill into the same agents as the default command above, but skips the CLI binary (use this to update or reinstall just the skill):
npx -y @mvanhorn/printing-press-library install zoho-expense --skill-onlyTo constrain the skill install to one or more specific agents (repeatable — agent names match the skills CLI):
npx -y @mvanhorn/printing-press-library install zoho-expense --agent claude-code
npx -y @mvanhorn/printing-press-library install zoho-expense --agent claude-code --agent codexIf npx isn't available (no Node, offline), install the CLI directly via Go (requires Go 1.26.3 or newer):
go install github.com/mvanhorn/printing-press-library/library/productivity/zoho-expense/cmd/zoho-expense-pp-cli@latestThis installs the CLI only — no skill.
Download a pre-built binary for your platform from the latest release. On macOS, clear the Gatekeeper quarantine: xattr -d com.apple.quarantine <binary>. On Unix, mark it executable: chmod +x <binary>.
Install the CLI binary first. The installer writes binaries to a per-user managed bin directory by default: $HOME/.local/bin on macOS/Linux and %LOCALAPPDATA%\Programs\PrintingPress\bin on Windows.
npx -y @mvanhorn/printing-press-library install zoho-expense --cli-onlyThen install the focused Hermes skill.
From the Hermes CLI:
hermes skills install mvanhorn/printing-press-library/cli-skills/pp-zoho-expense --forceInside a Hermes chat session:
/skills install mvanhorn/printing-press-library/cli-skills/pp-zoho-expense --forceRestart the Hermes session or gateway if the newly installed skill is not visible immediately.
Install both the CLI binary and the focused OpenClaw skill. The installer defaults binaries to a per-user bin directory ($HOME/.local/bin on macOS/Linux, %LOCALAPPDATA%\Programs\PrintingPress\bin on Windows):
npx -y @mvanhorn/printing-press-library install zoho-expense --agent openclawRestart the OpenClaw session or gateway if the newly installed skill is not visible immediately.
This CLI ships an MCPB bundle — Claude Desktop's standard format for one-click MCP extension installs (no JSON config required).
The bundle reuses your local OAuth tokens — authenticate first if you haven't:
zoho-expense-pp-cli auth loginTo install:
- Download the
.mcpbfor your platform from the latest release. - Double-click the
.mcpbfile. Claude Desktop opens and walks you through the install. - Fill in
ZOHO_EXPENSE_ORGANIZATION_IDwhen Claude Desktop prompts you.
Requires Claude Desktop 1.0.0 or later. Pre-built bundles ship for macOS Apple Silicon (darwin-arm64) and Windows (amd64, arm64); for other platforms, use the manual config below.
Manual JSON config (advanced)
If you can't use the MCPB bundle (older Claude Desktop, unsupported platform), install the MCP binary and configure it manually.
go install github.com/mvanhorn/printing-press-library/library/productivity/zoho-expense/cmd/zoho-expense-pp-mcp@latestAdd to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"zoho-expense": {
"command": "zoho-expense-pp-mcp",
"env": {
"ZOHO_EXPENSE_ORGANIZATION_ID": "<your-key>"
}
}
}
}Zoho Expense uses OAuth 2.0 with a self-client authorization-code flow. Create a self-client at https://api-console.zoho.in/, generate a 10-minute authorization code, then run zoho-expense-pp-cli auth login --client-id <id> --client-secret <secret> (browser callback flow). The CLI exchanges the code for a long-lived refresh token (stored at ~/.config/zoho-expense-pp-cli/config.toml) and refreshes access tokens transparently. Tokens use the literal Zoho-oauthtoken Authorization prefix (not Bearer).
# Exchange the 10-min code for a refresh token
zoho-expense-pp-cli auth login --client-id $ZOHO_EXPENSE_CLIENT_ID --client-secret $ZOHO_EXPENSE_CLIENT_SECRET
# Set the active organization (first one in the list)
zoho-expense-pp-cli org use $(zoho-expense-pp-cli organizations list --json | jq -r '.[0].organization_id')
# Sync categories, reporting tags, projects, customers, expenses into the local store
zoho-expense-pp-cli sync
# Upload one receipt with hash-dedup and auto-tag
zoho-expense-pp-cli receipt upload ~/Downloads/uber-receipt.pdf --auto-tag
# Batch ingest a folder for AI consumption
zoho-expense-pp-cli invoice ingest ~/Downloads/october-invoices --auto-tag --agent
# Bundle the month into a report and submit
zoho-expense-pp-cli close --month 2026-10 --auto-submit
These capabilities aren't available in any other tool for this API.
-
invoice ingest— Batch upload a folder of invoices to Zoho Expense, SHA256-dedup against the local store, poll autoscan in parallel, and auto-tag from learned merchant memory — the headline workflow for AI agents that ingest invoices from email.Agents that ingest invoices on a cadence need one command that handles dedup, autoscan, and tagging — not three.
zoho-expense-pp-cli invoice ingest ~/Downloads/october-invoices --auto-tag --agent -
receipt upload— SHA256-hash incoming receipts at upload, refuse duplicates (use --force to override).Idempotent receipt upload — safe to re-run on a folder agents have already processed.
zoho-expense-pp-cli receipt upload ~/Downloads/uber-receipt.pdf --auto-tag -
expense-untagged— List expenses missing category_id or project_id;--auto-fixapplies the merchant→tag memory map and PUTs back to Zoho.One command turns 30 untagged scanned receipts into 30 tagged ones, using the user's own historical mapping.
zoho-expense-pp-cli expense-untagged --auto-fix --agent
-
merchant list— Build a local merchant→category/project/tag mapping from sync history; pre-fill tags on first sight of a new merchant.Solves Zoho's 'first-time merchant is always uncategorized' limitation with zero API calls.
zoho-expense-pp-cli merchant list --agent --select merchant_name,category_name,seen_count
-
merchant map— Train the local merchant→category mapping for future auto-tag (one merchant at a time, or bulk from a CSV).Lets the user (or agent) explicitly seed the auto-tag map without waiting for sync history to learn it.
zoho-expense-pp-cli merchant map 'AWS' --category Software --project Engineering
-
close— Bundle a month's unreported expenses into an expense report in one shot — flags still-processing autoscans, flags untagged items, creates the report, attaches, optionally submits.The natural end of every monthly agent workflow — turn raw expenses into a submitted report without leaving the terminal.
zoho-expense-pp-cli close --month 2026-10 --auto-submit --agent
-
gst-split— India-specific: parse an expense's tax_id and line items, compute CGST/SGST (intra-state) or IGST (inter-state) shares, emit the breakdown or update the expense.Turns a monthly export into something a CA can directly file, without the user re-doing the math by hand.
zoho-expense-pp-cli gst-split exp_1234567890 --emit-csv
Run zoho-expense-pp-cli --help for the full command reference and flag list.
Currencies and exchange rates
zoho-expense-pp-cli currencies create- Add a currency to the orgzoho-expense-pp-cli currencies delete- Delete a currencyzoho-expense-pp-cli currencies get- Get a currencyzoho-expense-pp-cli currencies list- List currencies configured in the orgzoho-expense-pp-cli currencies update- Update a currency
Customers (contacts) expenses can be billed to
zoho-expense-pp-cli customers create- Create a customerzoho-expense-pp-cli customers delete- Delete a customerzoho-expense-pp-cli customers get- Get a customerzoho-expense-pp-cli customers list- List customerszoho-expense-pp-cli customers update- Update a customer
Expense categories used to classify expenses
zoho-expense-pp-cli expense_categories create- Create an expense categoryzoho-expense-pp-cli expense_categories delete- Delete an expense categoryzoho-expense-pp-cli expense_categories disable- Disable a categoryzoho-expense-pp-cli expense_categories enable- Enable a categoryzoho-expense-pp-cli expense_categories get- Get an expense categoryzoho-expense-pp-cli expense_categories list- List expense categorieszoho-expense-pp-cli expense_categories update- Update an expense category
Expense reports — bundles of expenses submitted for approval and reimbursement
zoho-expense-pp-cli expense_reports approval-history- View the approval history of a reportzoho-expense-pp-cli expense_reports approve- Approve an expense reportzoho-expense-pp-cli expense_reports create- Create an expense reportzoho-expense-pp-cli expense_reports get- Get an expense report (includes attached expenses)zoho-expense-pp-cli expense_reports list- List expense reportszoho-expense-pp-cli expense_reports reimburse- Mark an expense report as reimbursedzoho-expense-pp-cli expense_reports reject- Reject an expense reportzoho-expense-pp-cli expense_reports update- Update an expense report — used to attach more expenses
Expenses — the primary entity for an expense management CLI
zoho-expense-pp-cli expenses create- Create an expense (JSON body — for receipt upload use 'receipt upload')zoho-expense-pp-cli expenses get- Get a single expensezoho-expense-pp-cli expenses list- List expenses with rich filters (date range, status, user, category, project)zoho-expense-pp-cli expenses merge- Merge multiple expenses (used to dedupe a scanned-receipt expense with a manual one)zoho-expense-pp-cli expenses update- Update an expense — used to add category/project/tags after autoscan
Zoho Expense organizations you have access to
zoho-expense-pp-cli organizations get- Get organization detailszoho-expense-pp-cli organizations list- List organizations accessible to the authenticated user
Projects expenses can be associated with
zoho-expense-pp-cli projects activate- Mark a project activezoho-expense-pp-cli projects create- Create a projectzoho-expense-pp-cli projects deactivate- Mark a project inactivezoho-expense-pp-cli projects delete- Delete a projectzoho-expense-pp-cli projects get- Get a projectzoho-expense-pp-cli projects list- List projectszoho-expense-pp-cli projects update- Update a project
Upload receipts for autoscan
zoho-expense-pp-cli receipts- Upload a receipt file (multipart). Server queues for autoscan; pollexpenses getfor status.
Reporting tags (custom tagging schema for expenses, e.g. cost center, billable, GST treatment)
zoho-expense-pp-cli reporting_tags activate- Activate a reporting tagzoho-expense-pp-cli reporting_tags create- Create a reporting tagzoho-expense-pp-cli reporting_tags deactivate- Deactivate a reporting tagzoho-expense-pp-cli reporting_tags delete- Delete a reporting tagzoho-expense-pp-cli reporting_tags get- Get a reporting tagzoho-expense-pp-cli reporting_tags list- List reporting tagszoho-expense-pp-cli reporting_tags list-options- List all options for a reporting tagzoho-expense-pp-cli reporting_tags update- Update a reporting tag
Taxes (GST in India) applied to expenses
zoho-expense-pp-cli taxes create- Create a taxzoho-expense-pp-cli taxes delete- Delete a taxzoho-expense-pp-cli taxes get- Get a taxzoho-expense-pp-cli taxes list- List taxeszoho-expense-pp-cli taxes update- Update a tax
Business trips that group related expenses
zoho-expense-pp-cli trips approve- Approve a tripzoho-expense-pp-cli trips cancel- Cancel a tripzoho-expense-pp-cli trips close- Close a trip (no further expenses can be added)zoho-expense-pp-cli trips create- Create a tripzoho-expense-pp-cli trips delete- Delete a tripzoho-expense-pp-cli trips get- Get a tripzoho-expense-pp-cli trips list- List tripszoho-expense-pp-cli trips reject- Reject a tripzoho-expense-pp-cli trips update- Update a trip
Manage users in the organization
zoho-expense-pp-cli users activate- Mark a user as activezoho-expense-pp-cli users deactivate- Mark a user as inactivezoho-expense-pp-cli users delete- Delete a userzoho-expense-pp-cli users get- Get a userzoho-expense-pp-cli users invite- Invite a user into the orgzoho-expense-pp-cli users list- List userszoho-expense-pp-cli users me- Get the authenticated user's profilezoho-expense-pp-cli users update- Update a user
# Human-readable table (default in terminal, JSON when piped)
zoho-expense-pp-cli currencies list
# JSON for scripting and agents
zoho-expense-pp-cli currencies list --json
# Filter to specific fields
zoho-expense-pp-cli currencies list --json --select id,name,status
# Dry run — show the request without sending
zoho-expense-pp-cli currencies list --dry-run
# Agent mode — JSON + compact + no prompts in one flag
zoho-expense-pp-cli currencies list --agentThis CLI is designed for AI agent consumption:
- Non-interactive - never prompts, every input is a flag
- Pipeable -
--jsonoutput to stdout, errors to stderr - Filterable -
--select id,namereturns only fields you need - Previewable -
--dry-runshows the request without sending - Explicit retries - add
--idempotentto create retries and--ignore-missingto delete retries when a no-op success is acceptable - Confirmable -
--yesfor explicit confirmation of destructive actions - Piped input - write commands can accept structured input when their help lists
--stdin - Offline-friendly - sync/search commands can use the local SQLite store when available
- Agent-safe by default - no colors or formatting unless
--human-friendlyis set
Exit codes: 0 success, 2 usage error, 3 not found, 4 auth error, 5 API error, 7 rate limited, 10 config error.
This CLI owns bounded freshness for registered store-backed read command paths. In --data-source auto mode, covered commands check the local SQLite store before serving results; stale or missing resources trigger a bounded refresh, and refresh failures fall back to the existing local data with a warning. --data-source local never refreshes, and --data-source live reads the API without mutating the local store.
Set ZOHO_EXPENSE_NO_AUTO_REFRESH=1 to disable the pre-read freshness hook while preserving the selected data source.
Covered command paths:
zoho-expense-pp-cli currencieszoho-expense-pp-cli currencies createzoho-expense-pp-cli currencies deletezoho-expense-pp-cli currencies getzoho-expense-pp-cli currencies listzoho-expense-pp-cli currencies updatezoho-expense-pp-cli customerszoho-expense-pp-cli customers createzoho-expense-pp-cli customers deletezoho-expense-pp-cli customers getzoho-expense-pp-cli customers listzoho-expense-pp-cli customers updatezoho-expense-pp-cli expense_categorieszoho-expense-pp-cli expense_categories createzoho-expense-pp-cli expense_categories deletezoho-expense-pp-cli expense_categories disablezoho-expense-pp-cli expense_categories enablezoho-expense-pp-cli expense_categories getzoho-expense-pp-cli expense_categories listzoho-expense-pp-cli expense_categories updatezoho-expense-pp-cli expense_reportszoho-expense-pp-cli expense_reports approval_historyzoho-expense-pp-cli expense_reports approvezoho-expense-pp-cli expense_reports createzoho-expense-pp-cli expense_reports getzoho-expense-pp-cli expense_reports listzoho-expense-pp-cli expense_reports reimbursezoho-expense-pp-cli expense_reports rejectzoho-expense-pp-cli expense_reports updatezoho-expense-pp-cli expenseszoho-expense-pp-cli expenses createzoho-expense-pp-cli expenses getzoho-expense-pp-cli expenses listzoho-expense-pp-cli expenses mergezoho-expense-pp-cli expenses updatezoho-expense-pp-cli organizationszoho-expense-pp-cli organizations getzoho-expense-pp-cli organizations listzoho-expense-pp-cli projectszoho-expense-pp-cli projects activatezoho-expense-pp-cli projects createzoho-expense-pp-cli projects deactivatezoho-expense-pp-cli projects deletezoho-expense-pp-cli projects getzoho-expense-pp-cli projects listzoho-expense-pp-cli projects updatezoho-expense-pp-cli reporting_tagszoho-expense-pp-cli reporting_tags activatezoho-expense-pp-cli reporting_tags createzoho-expense-pp-cli reporting_tags deactivatezoho-expense-pp-cli reporting_tags deletezoho-expense-pp-cli reporting_tags getzoho-expense-pp-cli reporting_tags listzoho-expense-pp-cli reporting_tags list_optionszoho-expense-pp-cli reporting_tags updatezoho-expense-pp-cli taxeszoho-expense-pp-cli taxes createzoho-expense-pp-cli taxes deletezoho-expense-pp-cli taxes getzoho-expense-pp-cli taxes listzoho-expense-pp-cli taxes updatezoho-expense-pp-cli tripszoho-expense-pp-cli trips approvezoho-expense-pp-cli trips cancelzoho-expense-pp-cli trips closezoho-expense-pp-cli trips createzoho-expense-pp-cli trips deletezoho-expense-pp-cli trips getzoho-expense-pp-cli trips listzoho-expense-pp-cli trips rejectzoho-expense-pp-cli trips updatezoho-expense-pp-cli userszoho-expense-pp-cli users activatezoho-expense-pp-cli users deactivatezoho-expense-pp-cli users deletezoho-expense-pp-cli users getzoho-expense-pp-cli users invitezoho-expense-pp-cli users listzoho-expense-pp-cli users mezoho-expense-pp-cli users update
JSON outputs that use the generated provenance envelope include freshness metadata at meta.freshness. This metadata describes the freshness decision for the covered command path; it does not claim full historical backfill or API-specific enrichment.
zoho-expense-pp-cli doctorVerifies configuration, credentials, and connectivity to the API.
Config file: ~/.config/zoho-expense-pp-cli/config.toml
Static request headers can be configured under headers; per-command header overrides take precedence.
Environment variables:
| Name | Kind | Required | Description |
|---|---|---|---|
ZOHO_EXPENSE_CLIENT_ID |
auth_flow_input | Yes | OAuth client ID from api-console.zoho.in self-client |
ZOHO_EXPENSE_CLIENT_SECRET |
auth_flow_input | Yes | Set during initial auth setup. |
ZOHO_EXPENSE_REFRESH_TOKEN |
harvested | Yes | Populated automatically by auth login. |
ZOHO_EXPENSE_ORGANIZATION_ID |
per_call | Yes | Zoho Expense organization ID (sent as X-com-zoho-expense-organizationid header on every request) |
Authentication errors (exit code 4)
- Run
zoho-expense-pp-cli doctorto check credentials - Verify the environment variable is set:
echo $ZOHO_EXPENSE_ORGANIZATION_IDNot found errors (exit code 3) - Check the resource ID is correct
- Run the
listcommand to see available items
- HTTP 401 'authentication value invalid' — Run
zoho-expense-pp-cli auth refresh— the access token expired (1h lifetime). If that fails, the refresh token may have rotated out (max 20 per user); re-runauth loginwith a fresh authorization code. - HTTP 404 / endpoint not found from a .com token — Region mismatch. Your token was issued by accounts.zoho.com but you're targeting www.zohoapis.in. Override
base_urlin the config file or set ZOHO_EXPENSE_REGION accordingly. - Receipt upload returns 'autoscan_status: Failed' — File format or size issue — max 5MB; JPG/PNG/PDF only. Multi-page PDFs scan but slower. Try re-saving the PDF as image-only and retry.
- First expense for a new merchant has no category — Expected — Zoho only auto-categorizes after the merchant has been seen + categorized once. Use
merchant map '<name>' --category ...to seed the local mapping, thenexpense-untagged --auto-fix. - HTTP 429 rate-limited — Zoho caps at 100 req/min per org. The CLI's adaptive limiter handles this automatically; if you see this surface, you're running parallel batches — drop concurrency.
This CLI was built by studying these projects and resources:
- schmorrison/Zoho — Go (39 stars)
- ramp-cli — Python (28 stars)
- tdesposito/pyZohoAPI — Python (8 stars)
- airbyte source-zoho-expense — Python
Generated by CLI Printing Press