crxpaydocs

Troubleshooting

Every error you might hit, what causes it, and how to fix it.

This page is organised by symptom — search for the error message you're seeing.

Setup & manifest

"Cannot use import statement outside a module"

Your background script is loading as a classic script. Add "type": "module" to your manifest:

"background": {
  "service_worker": "background.js",
  "type": "module"
}

Then reload the extension in chrome://extensions. If you bundle, ensure your bundler outputs ESM (Webpack: experiments.outputModule: true; Vite: format: 'es').

"Refused to connect to https://api.crxpay.io/..."

Your extension doesn't have permission to talk to our API. Add:

"host_permissions": ["https://api.crxpay.io/*"]

After adding the permission, fully remove and reinstall the extension. Chrome doesn't always re-prompt for new host permissions on reload.

"Service worker registration failed"

99% of the time this is a syntax error in background.js that Chrome's MV3 loader silently swallows. Open the service worker console and look for the actual error.

The other 1%: you imported a file with a top-level await that fails. Wrap risky init code in a try/catch:

try {
  const client = CrxPay.configure({ apiKey: '...' });
} catch (err) {
  console.error('crxpay init failed', err);
}

Auth & identity

INVALID_API_KEY returned from every call

Three checks:

  1. The key starts with crxpay_pub_ — it's the public API key from the dashboard, not the Stripe key.
  2. The key matches the extension you're testing — each extension has its own key, and using the wrong one fails auth.
  3. You're not using a deleted/rotated key — check Settings → API keys in the dashboard.

CUSTOMER_NOT_IDENTIFIED

The SDK method you called requires a customer (e.g. setAttributes). Call CrxPay.identify(email) first.

Subscription state

"User paid but hasEntitlement('pro') returns false"

In order, check:

  1. Did the webhook fire? Look for [stripe-webhook] event ... type=customer.subscription.created in your API logs. If you don't see it, the Stripe CLI isn't forwarding events — see the next section.
  2. Did the entitlement get granted? Open Customers → that customer in the dashboard. The Entitlements tab should show pro with an expiry. If empty, the product wasn't mapped to the entitlement — check Entitlements in the dashboard.
  3. Is the SDK showing stale cache? Force a sync: await CrxPay.syncSubscription(). If it now returns the right state, your cache TTL is fine; the user just needed a refresh.

Webhooks not arriving in local dev

Stripe Connect events come from the developer's connected account, not your platform account. The default stripe listen --forward-to ... only forwards platform events.

Run this instead:

stripe listen \
  --forward-to        localhost:8790/v1/webhooks/stripe \
  --forward-connect-to localhost:8790/v1/webhooks/stripe

The CLI prints two webhook signing secrets — one for platform, one for Connect. The Connect secret is what test-mode subscription events sign with. Put it in apps/api/.dev.vars as STRIPE_WEBHOOK_SECRET_TEST.

getSubscription() returns status: 'unknown'

The SDK couldn't reach the API and has no usable cache. Three causes:

  1. Wrong host permissions — see "Refused to connect" above.
  2. Cache tampered — the HMAC verification failed and we discarded it. Look in the worker console for CACHE_TAMPERED. Fix: have the user uninstall and reinstall, or programmatically wipe the SDK's storage key.
  3. Brand new install with no network — totally expected. The next call with network access will resolve.

You can detect this and render a graceful UI:

const result = await CrxPay.getSubscription();
if (result.ok && result.data.status === 'unknown') {
  showSubscribePromptWithRetry();
}

Checkout

"Stripe secret key is a placeholder"

Your apps/api/.dev.vars still has sk_test_YOUR_KEY_HERE. Replace it with a real test key from your Stripe CLI config (~/.config/stripe/config.tomltest_mode_api_key) and fully restart wrangler dev — env vars are cached at startup.

"customer_creation can only be used in payment mode"

Stripe rejects customer_creation in subscription-mode checkout sessions (subscriptions auto-create the customer). If you see this and you're on a recent SDK/backend, pull the latest — we removed the offending param in apps/api/src/lib/stripe.ts.

Checkout opens then immediately closes

Pop-up blocker. Make sure you're calling openCheckout() from a user-initiated event (a click handler), not in response to a programmatic event. Chrome blocks chrome.tabs.create calls that aren't traceable to a user gesture.

Customer paid but doesn't show up in the dashboard

Same root cause as "Webhooks not arriving in local dev" above — the webhook never reached your API, so we never created the customer row. Once you fix the forwarding, you can replay the missed event:

stripe events list --connected-account acct_YOUR_ID --limit 5
stripe events resend evt_XXXXX --connected-account acct_YOUR_ID

The customer.subscription.created replay will create the customer + subscription rows.

SDK behaviour

onPaid fires twice

You called CrxPay.onPaid.addListener(...) from both background and popup. That's fine — they're independent listeners. If you only want to react once, register in one place and use chrome.runtime.sendMessage to broadcast to others.

onPaid doesn't fire after a successful test payment

Likely the webhook didn't arrive (see Connect forwarding above). The event only fires after the API processes customer.subscription.created and pushes a state update.

To debug, force a sync from the popup right after the user lands on the success page:

window.addEventListener('focus', async () => {
  await CrxPay.syncSubscription();
});

Extension reloaded → all SDK calls return EXTENSION_CONTEXT_INVALID

The user reloaded the extension while a content script was still alive. The content-script SDK returns this typed error instead of throwing — handle it with a "please refresh this page" banner:

const result = await CrxPay.getSubscription();
if (!result.ok && result.error.code === 'EXTENSION_CONTEXT_INVALID') {
  showRefreshBanner();
  return;
}

Dashboard

"Stripe account connected" but I can't create products

Stripe needs to fully verify your account before you can create live-mode products. Test mode works immediately. Click the Continue verification prompt in the Stripe Express dashboard.

My customer's email shows as stripe-cus_xxxxx@unknown.com

The webhook fired before we could fetch the customer's email from Stripe. Almost always a transient network blip. Click Resync from Stripe on the customer detail page (or replay the original event with stripe events resend).

Still stuck?

Was this page helpful?

Your feedback shapes what we document next.