Custom UI extension (floating window + panel) never loads — surface bootstrap iframe has empty userId/companyId

Hi all, working on an app with AI and ran into some trouble. Here it is, thanks in advance!

I’m building a private app with two custom UI extensions (custom floating window and a custom panel on deal details), both pointing to the same HTTPS URL. Neither loads — the floating window spins forever, the panel shows “Something went wrong… Please refresh the panel.”

What I found investigating via browser devtools:

Before ever requesting my app’s own URL, Pipedrive loads a bootstrap iframe pointing to its own domain:

https://<company>.pipedrive.com/api/v1/marketplace-extensions/surfaces/{extension_id}?resource=deal&view=details&userId&companyId&id=xxx&theme=light

Note userId and companyId are present as parameter names but carry no value. My app’s own URL is never requested at all — confirmed with network monitoring (zero requests to my domain). This happens identically for both extension types.

What I’ve ruled out so far:

  • OAuth scopes: Pipedrive support suggested requesting only necessary scopes. I narrowed mine to the real minimum (Deals/Activities/Contacts/Leads, read-only) and re-authorized — confirmed the new scopes actually took effect via “View authorization info” in the installed-apps UI. No change in behavior.
  • Draft vs. live: Switched the app from draft to live. No change.
  • Credentials: OAuth token exchange itself works correctly (client_id/secret are valid) — this is purely about the surface bootstrap step, which happens before any request reaches my backend.
  • Extension config: URL, placement (resource: deal, view: details), and app installation status all look correct in the API response for GET /api/v1/marketplace/apps/extensions.

Has anyone run into this specific symptom — empty userId/companyId in the surface bootstrap URL? Is there some additional setup step for custom UI extensions I might be missing?

App client_id: 08a36b16807252b4 (private app, sandbox account).

Resolved — the bug was on my side, not Pipedrive’s.

Posting the root cause in case it saves someone else the time.

Symptom: custom floating window spins forever, custom panel shows “Something went wrong”. No requests ever appeared to reach my app.

Actual cause: @pipedrive/app-extensions-sdk is a CommonJS package (main: ./dist/index.js, no exports/module field) that exports the class as exports.default. Bundlers like Vite/esbuild apply Node-style ESM interop, where import X from '<cjs-package>' gives you the whole module.exports — so the class ends up one level deeper, at X.default. My new AppExtensionsSDK() was therefore throwing:

TypeError: y.default is not a constructor

Because the SDK handshake never ran, Pipedrive kept waiting and showed its own spinner indefinitely. That made it look like a platform issue when it was entirely my bundling problem.

Fix:

import AppExtensionsSDKImport, { Command, Event } from '@pipedrive/app-extensions-sdk';

// CJS/ESM interop: depending on the bundler, the class may be the default
// export itself or one level deeper. Unwrap both.
const AppExtensionsSDK = (AppExtensionsSDKImport as any).default ?? AppExtensionsSDKImport;

const sdk = new AppExtensionsSDK();
await sdk.initialize();

Note TypeScript cannot catch this — the shipped types declare the class as the default export, so new AppExtensionsSDK() typechecks fine and only fails at runtime.

Two red herrings worth flagging:

  1. The surface bootstrap iframe (/api/v1/marketplace-extensions/surfaces/{id}?...&userId&companyId&...) has empty userId/companyId params. That appears to be normal and is not related to this failure — I wasted a lot of time on it.
  2. If your init error is swallowed (e.g. a .catch() that just renders a fallback), you get zero signal. Logging the actual error from the SDK init made the cause obvious within seconds.

Debugging tip: the fastest way to isolate this was a tiny local harness — a plain HTML page that embeds the app in an iframe with ?id=test, listens for message events and replies on the attached MessagePort. It immediately showed my app was sending nothing at all, with no dependency on Pipedrive.

Thanks, and sorry for the noise — no further help needed here.