Operator runbook

Symptom → fix.

For the person who runs SideQuest for a distributor. Type a symptom in the box, hit the matching card. End users should check the FAQ first.

Quick reference — commands you'll run most

  • sidequest doctor health check
  • sidequest reauth-qb refresh QB token
  • sidequest setup install wizard
  • sidequest demo verify install
  • sidequest flush-usage send usage events
  • tail ~/.qb-distributor-mcp/auth.log token timeline

Health + setup

Nothing seems to be happening. Claude can't see SideQuest at all.

  1. Cmd+Q Claude Desktop (not just close the window).
  2. Open Terminal and confirm the binary resolves:
    which sidequest
    Should return a venv path. If empty, open a fresh Terminal window or source ~/.zshrc.
  3. Reopen Claude Desktop. Look for SideQuest Automation in the MCP tool list at the bottom of the compose box.
  4. Still not there? Run sidequest doctor and follow the output.
Why this happens

Claude Desktop spawns the MCP server on stdio at launch. If the claude_desktop_config.json file is malformed or the connector binary isn't on PATH, the server never starts and Claude has no tools to call. The legacy binary name was qb-distributor-mcp (pre-v0.15.15); both names still work as aliases.

Need to run a self-diagnostic.

sidequest doctor

Prints license tier, env vars, file presence, license check, demo-mode call, and the QB refresh-token chain probe. Output is safe to paste into a support ticket — license key and tokens are redacted automatically.

QuickBooks auth

"QuickBooks token needs reseeding" / error 3200 / 401 / Token expired

sidequest reauth-qb

Walks the Intuit OAuth dance via the hosted callback at sidequestautomation.com/qb/callback, writes the fresh refresh token to .env, and auto-reinjects into Claude Desktop's config. Then Cmd+Q Claude Desktop and reopen.

From v0.15.13 the token rotates itself going forward. This is a one-time reseed.

Before reseeding, check the audit log

v0.15.27+ writes every QB refresh attempt to ~/.qb-distributor-mcp/auth.log. Token tails are redacted. Run tail -20 ~/.qb-distributor-mcp/auth.log to see whether the chain died because Intuit rejected a rotation or because a stale token was retried after a process restart. The two scenarios have different fixes.

Want to see the QB refresh history.

tail -20 ~/.qb-distributor-mcp/auth.log

v0.15.27+ writes every refresh attempt with ISO timestamp, event name (refresh_start, refresh_success, refresh_fail, persist_write), and redacted token tail (...4XYZ (17 chars)). File rotates at 1 MB to auth.log.1.bak.

QB error 2020 — Permissions error on Estimates.

sidequest reauth-qb

The OAuth scope on Estimates got revoked. The reauth flow re-issues the grant with com.intuit.quickbooks.accounting scope. Cmd+Q Claude Desktop and reopen after.

Error 3200 keeps coming back even after reauth.

On v0.15.27+ this is rare. Open ~/.qb-distributor-mcp/auth.log — the timeline tells the story. Three remaining causes:

  • The grant was revoked in QuickBooks (admin removed the app, password reset, trial company deleted).
  • QB_ENVIRONMENT was swapped (sandbox ↔ production) without re-authing.
  • Multiple connector instances are racing each other to refresh.

Re-run sidequest reauth-qb once more. If 3200 returns within an hour, send auth.log + sidequest doctor output to support.

QuickBooks submit + writes

Submit returned freight_unconfigured.

  1. In QuickBooks: Sales → Products and services → New → Service. Name it Shipping. Save.
  2. Open the item again. The URL contains the item id (the number after /item/). Copy it.
  3. Add to ~/.qb-distributor-mcp/.env:
    SIDEQUEST_FREIGHT_ITEM_ID=<the-id>
  4. Run sidequest reauth-qb (which also reinjects). Cmd+Q Claude Desktop and reopen.

Resubmit the draft. Freight now lands as a real SalesItemLineDetail referencing the Shipping item.

Draft has both a doc discount AND freight. What does QB store?

On v0.15.29 or later: products get discounted, freight stays at full carrier cost. Local and QB totals agree to the penny.

How it gets there: the connector pre-computes the discount as a fixed dollar amount (lines_subtotal × pct ÷ 100) and submits a DiscountLineDetail with PercentBased: false and that dollar amount filled in. QB stores it as-is and doesn't recalculate, so freight never gets swept into the discount.

What you'll see in the QuickBooks UI: the Estimate shows the discount as a dollar amount (e.g. $36.45 off), not 5% off. The percent intent is written into the discount line's Description ("5% off products ($36.45)"), so the printed Estimate the customer sees still calls out the 5%. Operators still set the discount as a percent in Claude — the percent-to-dollars conversion happens only at QB submit time.

Why the v0.15.25 / v0.15.28 fixes didn't take

v0.15.25 reordered the payload to products → discount → freight; v0.15.28 stamped LineNum on every line. Both assumed QB would apply a percent discount only to lines preceding the discount entry. It doesn't — when PercentBased: true, QuickBooks Online always applies the percent to the entire document subtotal, regardless of line position or LineNum. Four consecutive sandbox Estimates (1010-1013) reproduced the $716.30 vs $717.55 drift with both attempts in place. v0.15.29's fixed-dollar serialization is what actually fixed it; the next Estimate (1014) stored $717.55 to the penny with no qb_total_mismatch. The LineNum stamps stayed in — harmless, useful for audit — but they aren't the fix.

Why "freight outside discount" is the right default

Freight is a pass-through cost. NetSuite, Acumatica, SAP Business One, and Fishbowl all default the same way. If a customer needs freight discounted on a specific Estimate (rare), edit the Estimate in QB after submit — the connector won't fight the override.

Submit response includes a qb_total_mismatch warning.

The QB-stored total disagrees with the locally-computed total by more than a penny. v0.15.29 ended the combined-discount-and-freight case for good — v0.15.25 had claimed the same and turned out not to be the fix (see the combined-discount card above for what actually happened). Remaining causes today:

  • A tax line QB injected that the draft didn't account for.
  • A rounding gap on a percentage discount (rare — the fixed-dollar serialization avoids most rounding paths).
  • The draft was edited between preview and submit.

Open the QB Estimate, find the line that doesn't agree, send draft_id + both totals + QB Estimate id to support. On v0.15.29+ the warning is rare; when it does surface, it's a real bug worth tracing.

QB error 5010 — Object not found on submit.

The customer name on the PO doesn't match any QB Customer. Either:

  • Create the customer in QB first, then resubmit.
  • Have Claude reassign the draft to an existing customer before submitting.

QB error 6240 — Duplicate transaction.

This PO number already has an Estimate in QB. Look up the existing one before resubmitting.

Matching + cross-references

SideQuest matched to the wrong SKU.

Add a row to ~/.qb-distributor-mcp/cross_reference.csv:

customer_id,customer_part,internal_sku,notes
ACME,ACME-EL12,BR-ELB-050-NPT,Acme uses their own codes

Then either Cmd+Q Claude Desktop and reopen, OR ask Claude to call refresh_catalog for a no-restart pickup.

Other causes

Description ambiguity — the customer's description matches multiple QB SKUs. Tighten by adding more detail to the QB item description.

Stale catalog cache — the connector caches QBO items at first call. If you added a SKU 30 min ago and Claude Desktop has been open all day, ask Claude to call refresh_catalog.

All matches come back low-confidence.

The cross-reference table is probably undersized. Check the row count:

wc -l ~/.qb-distributor-mcp/cross_reference.csv

Most distributors need at least 100 mappings before match quality stabilizes. If you have a customer who sends weird POs weekly, ship their last 6 months of POs as a CSV/ZIP to support — we'll extract the implicit cross-references in batch.

Customer field reads "Corporate AP Office" or similar — not the real buyer.

v0.15.16 added an AP-office fallback. When the parser detects an AP-system identifier in the Bill To, it walks: sender's email domain stem → vendor block → sender's display name. The draft gets tagged with customer_source="ap_fallback_domain" | "ap_fallback_vendor" | "ap_fallback_sender" so you can see how it was inferred.

If the fallback picks the wrong customer, override on the draft and add a cross-reference row so the next PO from that AP office anchors correctly.

Consumer-mail domain protection

Gmail, Yahoo, Outlook, Hotmail, AOL, iCloud, and Proton are explicitly denied from the domain-stem fallback. You'll never get a draft anchored to customer="Gmail" because the buyer used a personal address. The vendor and sender-name fallbacks still run.

Pricing

SideQuest flagged a price as wrong but it's actually right.

The QuickBooks list price for that item is stale. The connector compares PO line price against QB Item UnitPrice. Two fixes:

  • Update the QB Item's UnitPrice to match the real list.
  • Set up a customer-specific price level in QB — the connector will use it automatically.

Email + labeling

A PO was labeled in Gmail but Claude can't find it.

  1. Confirm the label name matches exactly. Default is purchase-orders. Open .env and check GMAIL_PO_LABEL.
  2. Nested label? Include the slash: POs/Incoming.
  3. Cmd+Q Claude Desktop and reopen so the connector re-reads .env.
  4. Still missing? Have Claude call parse_po_from_email with the message_id directly. If heuristic_lines is empty and raw_content is gibberish, the PDF isn't text-extractable — send to support and we'll add that format.

Claude keeps drafting the same PO over and over.

Ask Claude to call mark_email_processed on that message ID. The connector remembers processed message IDs in ~/.qb-distributor-mcp/drafts.sqlite.

A marketing/contest email got labeled as a PO.

v0.15.26+ added a promo filter. Emails with promotional emoji (🔥💰🎁🎯🎉), contest language ("Win a/the X", "sweepstake", "giveaway"), sales markers ("N% off", "Black Friday", "flash sale", "limited time"), newsletter hooks ("unsubscribe", "view in browser"), or known bulk-mail sender domains (mailchimp.com, sendgrid.net, klaviyo.com, etc.) get rejected pre-classification.

If a promo is still slipping through, send the message_id to support and we'll tighten the matcher.

Gmail token expired.

Rerun the Gmail OAuth dance directly:

~/.qb-distributor-mcp/venv/bin/python -c "from pathlib import Path; from qb_distributor_mcp.gmail_client import GmailClient; h=Path.home()/'.qb-distributor-mcp'; GmailClient(h/'google_client_secret.json', h/'google_token.json')"

If it fails with KeyError: 'client_secret', your google_client_secret.json is the incomplete first download. Go to Google Cloud → your OAuth client → + Add secret, download the second one, replace the file, rerun.

Reports + insights

Local reports come back empty or very thin.

Expected on recent installs. Local reports read ~/.qb-distributor-mcp/drafts.sqlite and usage.sqlite — only data from POs the connector processed since install.

For deeper history, use the QB pass-through reports: report_qb_top_items, report_qb_top_customers. They pull full QBO history.

report_pos_processed dollar value looks wrong.

v0.15.13+ splits the response into two source blocks. Pick the right one:

  • from_usage_log — append-only billing counters. Source of truth for "what was I billed for?"
  • from_drafts_store — current SQLite state. Reflects edits/deletions. Answers "what's on my board now?"

If they disagree, the usage log is the billing source of truth. The drafts store can drift if a draft was deleted post-submit or the local store got rebuilt.

report_match_quality shows a high flag rate.

Look at the top review reasons:

  • description_fuzzy or customer_part_not_found dominate → add cross-reference rows for that customer.
  • short_description dominates → customer's PO format isn't including line descriptions. Matcher needs the SKU column to be present.

Billing

"You've hit your monthly limit."

You're past your tier's monthly PO quota. Upgrade at sidequestautomation.com/pricing or email support for a custom plan.

Card on file got declined.

Stripe sends a dunning email automatically. Your service stays active for 7 days of grace. Update the card via the Stripe customer portal link in your most recent receipt email.

After 7 days of failed retries, your account drops to free tier (still functional, just rate-limited at 20 POs/month).

QuickBooks Desktop bridge (Windows beta)

"Couldn't reach QuickBooks Desktop" error.

Three causes. Check in order:

  1. QBD isn't open. The bridge needs QuickBooks Desktop running with the company file loaded.
  2. The bridge isn't running. Look for the start-bridge.bat command window on the taskbar. If closed, double-click start-bridge.bat in the connector folder.
  3. Trust dialog was rejected. QuickBooks → Edit → Preferences → Integrated Applications → tick "Allow this application to access your company file" for SideQuest Connector.

"QuickBooks Desktop is busy" / 409 from the bridge.

A modal dialog is open in QBD. Bring QBD to the front, close any open wizards, retry. The bridge auto-retries 3× with backoff before surfacing.

Bridge crashed and the command window closed.

QBSDK COM connections crash periodically — this is what the bridge model protects against. Just re-double-click start-bridge.bat. The MCP server stays alive across bridge restarts.

If it happens more than once a day, copy the last 50 lines of the bridge log window and email support.

Customer wants to switch QBO ↔ QBD.

Change QB_BACKEND=online or QB_BACKEND=desktop in .env. Cmd+Q Claude Desktop and reopen. No reinstall needed — local stores (drafts.sqlite, usage.sqlite, cross_reference.csv) are backend-agnostic.

No symptoms match. Try fewer words, or email support.

When to escalate. Email [email protected] with: what you tried, what happened instead, the output of sidequest doctor (safe to share — secrets are redacted), and if it's a PO issue, the sample PO. Response target during business hours (Mon-Fri, 9am-5pm ET): 2 hours for Scale tier, 1 business day for everyone else.