different-ai/openwork · error · Error

Billing portal failed (${response.status}).

Error message

Billing portal failed (${response.status}).

What it means

Thrown by openStripePortal when POST /v1/billing/stripe/portal returns a non-OK status. Like the other getRequestError sites, a 403 "reauth" payload is converted to ReauthRequiredError; otherwise the server message (or this status-annotated fallback) is thrown. It means the Stripe Customer Portal session could not be created server-side.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx:316

    } catch (error) {
      setStripeError(error instanceof Error ? error.message : "Could not start seat billing checkout.");
    } finally {
      setStripeActionBusy(null);
    }
  }

  async function openStripePortal() {
    if (!canManageBillingSettings) {
      setStripeError("Only workspace owners and super-admins can open billing portals from Settings.");
      return;
    }

    setStripeError(null);
    try {
      await runReauthableAction("billing-portal", async () => {
        setStripeActionBusy("portal");
        const { response, payload } = await requestJson("/v1/billing/stripe/portal", { method: "POST" }, 12000);
        if (!response.ok) throw getRequestError(payload, response, `Billing portal failed (${response.status}).`);
        const url = payload && typeof payload === "object" && "url" in payload && typeof payload.url === "string" ? payload.url : null;
        if (!url) throw new Error("Billing portal response did not include a URL.");
        window.location.href = url;
      });
    } catch (error) {
      setStripeError(error instanceof Error ? error.message : "Could not open the billing portal.");
    } finally {
      setStripeActionBusy(null);
    }
  }

  const showPolar = polarBilling?.hasActivePlan === true && Boolean(polarBilling.portalUrl);
  const stripePrice = stripeBilling ? formatMoneyMinor(stripeBilling.unitAmount, stripeBilling.currency) : null;
  const seatBilling = stripeBilling?.seats;
  const webBilling = stripeBilling?.web ?? null;
  const seatPrice = seatBilling ? formatMoneyMinor(seatBilling.unitAmount, seatBilling.currency) : null;
  const activeMemberCount = stripeBilling?.memberCount ?? 0;

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Read the surfaced server message to distinguish config vs auth failures.
  2. Handle reauth: retry after re-authenticating (runReauthableAction already wraps this).
  3. Enable the Stripe customer portal and its return URL in the Stripe dashboard for the connected account.
  4. Ensure the org has a Stripe customer record (portal requires one); subscribe first via checkout.
  5. Check Den server logs for Stripe API errors if 5xx.

Example fix

// before
} catch (error) {
  setStripeError(error instanceof Error ? error.message : "Could not open the billing portal.");
}
// after
} catch (error) {
  if (isReauthRequiredError(error)) { setStripeError("Session expired - sign in again."); return; }
  setStripeError(error instanceof Error ? error.message : "Could not open the billing portal.");
}
Defensive patterns

Strategy: try-catch

Validate before calling

const status = await requestJson("/v1/billing/status", {}, 10000);
if (!status.response.ok) return; // org has no billing record; portal will 4xx

Type guard

function isReauth(error: unknown): error is ReauthRequiredError { return error instanceof ReauthRequiredError; }

Try / catch

try {
  await runReauthableAction("billing-portal", openStripePortal);
} catch (error) {
  if (isReauthRequiredError(error)) { promptReauth(); return; }
  setStripeError(error instanceof Error ? error.message : "Could not open the billing portal.");
}

Prevention

When it happens

Trigger: POST /v1/billing/stripe/portal fails: org lacks a Stripe customer ID (never subscribed), Stripe portal not configured/enabled in the Stripe dashboard, expired session (401), reauth required (403), or Stripe API error (5xx).

Common situations: Clicking 'Manage billing' on a self-hosted Den without Stripe portal links configured; org created but never subscribed; Stripe portal deactivated in the Stripe dashboard; stale admin session.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/abcc850d8cd5bb70. Report an issue: GitHub.