Free Resource · guide

Sharing Your EBITDA Dashboard With Your Team: What Actually Breaks in Production

In Part 3 the dashboard worked, on one laptop, against a fake sandbox company. This is Part 4, where it has to work for real, hosted somewhere your team can reach, connected to your actual QuickBooks company, with production credentials instead of sandbox ones. That jump takes real hosting, a real production setup on Intuit's side, and, in our case, one stubborn bug that took a character-by-character diff to find.

This is Part 4 of a series on getting your EBITDA out of QuickBooks, from a number you calculate by hand to a live dashboard you can pull any time.

  1. What EBITDA is and how to get it from QuickBooks
  2. Connecting and testing the QuickBooks API
  3. Building your own EBITDA dashboard
  4. Sharing your dashboard with your team, you’re here.

Here’s where the series left off. Part 3 ended with a working dashboard, a real chart, real math, running on one laptop against a fake sandbox company. That’s a great prototype. It’s also not something your leadership team can open, because it lives on your machine, on localhost, talking to data that doesn’t exist.

Getting from there to “the whole team can open this and see real numbers” is two build steps and, in our case, one gotcha that ate the rest of the afternoon. Same as Part 3, Claude did the actual work here, clicking through hosting settings, reconfiguring the Intuit developer portal, writing and running the diagnostic scripts. Our job was to describe what “done” looked like, watch what it found, and sanity-check the result before trusting it, the same discipline from every article in this series, just aimed at infrastructure instead of a P&L this time.

What actually changes going to production

Quick orientation, because “production” means more than one thing here:

  • Hosting. The app needs to live somewhere reachable by a URL, not localhost, and it needs to stay running.
  • HTTPS, always. A dashboard that touches financial tokens has no business accepting plain http://.
  • A password gate, at minimum. Your dashboard doesn’t need a full user-accounts system on day one, but it does need something between it and the open internet. A shared password checked on every request is a reasonable starting point for a small internal tool. It’s not the ideal, though: it can’t tell one team member from another, and it can’t be revoked for one person without changing it for everyone. If this dashboard is going to more than a couple of people, per-user logins, or better, single sign-on through whatever your company already uses (Microsoft 365, Google Workspace), is worth setting up properly rather than leaning on one shared password long-term.
  • Production QuickBooks keys, on your real company. Production isn’t just “Development with different keys.” Several settings live only on the Production side of your Intuit developer app, and if you only configured them for Development while building in Part 3, Production silently doesn’t have them.

Step 1: Build out hosting

We put ours on an Azure Web App, but any always-on host works the same way. Two settings matter more than they get credit for:

  • HTTPS Only, turned on. Most hosts let plain http:// through by default unless you explicitly say not to. On Azure this is a single toggle in the app’s TLS/SSL settings, confirm it’s on rather than assuming it is.
  • Always On, turned on. Without it, a lot of hosts will spin your app down after a period of no traffic and take several seconds to wake back up on the next request, which is a rough first impression for anyone opening the dashboard cold. Always On keeps it warm.

If you’re repurposing an existing host rather than starting fresh, this is also the moment to check whether it has any leftover network restrictions from its previous life, an old IP allowlist, a firewall rule, anything that would quietly block traffic before it ever reaches your app.

Step 2: Get production keys

In your app on the Intuit developer portal, requesting production credentials is one click, and it shows you a real Client ID and Client Secret immediately. That can trick you into thinking you’re done, it isn’t quite that simple.

Intuit's Keys and Credentials page with the Get production keys panel

Underneath, Intuit tracks a readiness checklist, app details, a compliance questionnaire, meant for apps going on the public QuickBooks App Store.

Intuit's Required Steps checklist showing App details and Compliance incomplete

If you’re connecting your own company rather than publishing publicly, that checklist mostly doesn’t block you, ours turned out to already be submitted and approved from an earlier pass. Fill out what Intuit actually requires for your use case, and don’t get pulled into App Store listing details you don’t need yet.

Two more settings need attention here, both tracked separately for Development and Production even though they live under the same app:

  • App URLs. Host domain, launch URL, disconnect URL, connect/reconnect URL. Ours had generic placeholder values on the Production side, left over from initial app setup. Go through each field with the Production toggle selected and confirm every one points at your real hosted app.
  • Redirect URI. Back in Part 3 we registered http://localhost:3000/callback under Development, which is what made the sandbox flow work. Production has its own, separate redirect URI list, and ours had never gotten the real production callback address added to it.

The redirect URI list showing only test and localhost entries, missing the real production callback

Same rule as always: the address in Intuit’s settings has to match your app’s real callback URL character for character, checked against the Production tab specifically.

Gotcha: the error message won’t tell you what’s actually wrong

Here’s the frustrating part. Even after hosting was solid and Step 2 was fully filled out, clicking Connect still failed, with the same generic message every time:

“Uh oh, there’s a connection problem. Sorry, but undefined didn’t connect.”

Intuit's generic connection-failed error page

That message doesn’t name a cause. It doesn’t distinguish a bad redirect URI from an unconfigured App URL from a wrong Client ID. It’s the same page no matter what’s actually broken, which means you have to isolate the failure yourself rather than trust the error to point at it.

The move that actually works: test your own callback directly, bypassing Intuit’s authorize page entirely. Hit your app’s /connect route yourself to get it to generate a real, freshly-issued state value (your app logs or briefly shows this). Then call your app’s /callback route directly with that real state plus a made-up authorization code. Two outcomes tell you two different things:

  • If your app correctly rejects a mismatched state (one you didn’t just get from /connect), that confirms your CSRF protection is working.
  • If, with the real state, your app gets far enough to attempt a real token exchange with Intuit, and the error that comes back is something specific like invalid_grant / “Invalid authorization code,” rather than invalid_client, that’s Intuit telling you it recognized your Client ID and Secret and only rejected the fake code. In other words: your app and your credentials are fine. The real failure is happening earlier, on Intuit’s own authorize page, before it ever redirects back to you with a genuine code.

That single test told us the bug wasn’t in our code, or even in the Intuit settings we’d already fixed. One thing was left to check: does the Client ID our app is actually sending match, character for character, what Intuit’s portal shows for this app? This is the point in the afternoon where having Claude do the digging actually mattered, it ran the isolation test, read the portal, and wrote the comparison script without getting tired or starting to skim, which is exactly the failure mode that let the typo hide in the first place.

Intuit's production Client ID and Client Secret revealed on the Keys and Credentials page

It didn’t. Buried in the middle of a 50-character random string was one character that didn’t match: our app had a lowercase l, Intuit’s portal showed an uppercase I. On screen, in the font both were rendered in, those two characters are close to indistinguishable, which is exactly how the typo got in and exactly how it survived being looked at directly more than once.

The only way to actually catch this is to stop looking at it, and diff the two strings programmatically, character by character, comparing character codes rather than rendered glyphs:

const a = "...yourAppsValue...";
const b = "...portalsValue...";
for (let i = 0; i < Math.max(a.length, b.length); i++) {
  if (a[i] !== b[i]) {
    console.log(`DIFF at ${i}: "${a[i]}" (${a.charCodeAt(i)}) vs "${b[i]}" (${b.charCodeAt(i)})`);
  }
}

That’s a two-second script, and it’s the only method that can’t be fooled by an ambiguous font. It found exactly one difference, at exactly one position. Once the Client ID was corrected to match Intuit’s portal exactly, the connection worked on the very next attempt.

Where it probably came from: credentials get copied from a portal into an .env file, sometimes by hand, sometimes across a couple of tools, and a visually-identical character swap is an easy, silent way for that copy to go slightly wrong. There’s nothing to debug in the logic, the string is just wrong, which is exactly why “read the code” doesn’t catch it, and “compare the characters” does. If your own connection ever fails with a similarly vague error after everything else checks out, run that diff early rather than last, it would have saved us the most time here.

Connect to QBO, and here’s our data

With hosting solid, Step 2 fully configured, and the Client ID corrected, the dashboard connects cleanly to our real QuickBooks company, over HTTPS, behind a password, reachable by anyone on the team with the link, not just the laptop that built it.

The hosted dashboard's landing page: a Connect to QuickBooks button, labeled Production, Braintek internal use only

Clicking Connect now runs the full flow, real sign-in, real company picker, no error page:

The QuickBooks company-selection screen after a successful production authorization

And the dashboard itself renders real add-back rows and a real margin, on demand, for whichever date range you pick, no spreadsheet, no export, no waiting on an accountant. (The figures below are illustrative, not our actual numbers, the point is that the build-up, the math, and the layout all come from Part 1 and Part 3, just running live now instead of by hand.)

The finished production dashboard showing an EBITDA build-up and a 15.5% margin

The math is exactly what Part 1 taught you to do by hand and Part 3 automated, now running against real numbers, for real people, on demand.

If you’d rather skip straight past the character-by-character debugging and have hosting, HTTPS, production credentials, and a real connection set up correctly the first time, that’s squarely what we do. See our IT consulting in Houston or book a discovery call and tell us which numbers you want your whole team looking at.

Want your production connection working the first time, not the fifth?

Hosting, HTTPS, production QuickBooks credentials, and a connection that actually works, that's four separate things to get right. Tell us you run QuickBooks Online and we'll set your dashboard up correctly the first time, without the afternoon lost to an unhelpful error message.

By submitting, you agree to be contacted by Braintek about your inquiry.

FAQs

Why did my sandbox connection work but production didn't?

Development (sandbox) and Production are two entirely separate credential sets, redirect URI lists, and app-URL configurations inside your Intuit developer app, even though they live under the same App ID. Getting Development working proves your code is correct. It proves nothing about whether Production is configured, and in our case it wasn't, on several settings tabs at once.

What does "Sorry, but undefined didn't connect" actually mean?

Almost nothing, which is the problem. It's Intuit's generic catch-all error for the OAuth authorize step failing before it ever issues a real authorization code, and it fires for a wide range of underlying causes, a wrong redirect URI, an unconfigured App URL, or, in our case, a single mistyped character in the Client ID. The message doesn't tell you which. You have to isolate it yourself.

How do I tell if the problem is my app's code or Intuit's configuration?

Trigger your own callback directly. Hit your app's /connect route to generate a real, freshly-issued state value, then call your /callback route yourself with that real state plus a fake authorization code. If your app correctly rejects a mismatched state and, with the real one, gets as far as attempting a token exchange, error messages like invalid_grant instead of invalid_client tell you Intuit accepted your credentials and only rejected the fake code, meaning your app is fine and the real failure is happening earlier, on Intuit's authorize page itself.

How do you catch a one-character typo like that?

Not by reading it. On screen, a lowercase L and a capital I are often identical, which is exactly how this typo survived multiple reviews. The only reliable check is programmatic, pull both strings and compare them character by character with their actual character codes, not by eyeballing rendered text. It takes one script and a few seconds, and it's the only method that can't be fooled by a font.

Is a single shared password enough to protect a dashboard like this?

For a small internal tool it's a reasonable starting point, but it's the minimum, not the ideal. A shared password can't tell one team member from another, doesn't log who looked at what, and can't be revoked for one person without changing it for everyone. If more than a couple of people need access, or the data is sensitive enough to matter, per-user logins or, better, single sign-on through the identity provider your company already uses (Microsoft 365, Google Workspace) is worth the extra setup. It gives you real accounts, real revocation, and a real audit trail instead of one password everyone shares.

Did AI actually do the production troubleshooting in this article, not just the coding in Part 3?

Yes, all of it, reconfiguring the hosting settings, walking through the Intuit developer portal, and writing the isolation test and the character-diff script that found the typo. Same division of labor as the rest of this series, Claude does the clicking, testing, and writing, we describe what we want, review what it finds, and check its conclusions before trusting them. The character-diff step is a good example of why that combination works, it's the kind of tedious, easy-to-skim comparison that AI can run perfectly every time, while a human deciding whether the result actually makes sense, and whether it's safe to act on, is still the part that's on you.

Ready for IT that just works?

Book a no-pressure discovery call. We'll review your setup and show you exactly where you stand.