different-ai/openwork · error · Error

Billing portal response did not include a URL.

Error message

Billing portal response did not include a URL.

What it means

openStripePortal POSTs to /v1/billing/stripe/portal expecting a JSON body containing a string 'url' pointing at Stripe's billing portal session. The error is thrown when the response is 200 OK but no valid url field is present, so the redirect cannot happen.

Source

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

    } 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;

  // Without Stripe keys the deployment never charges for either product, so the
  // page must not quote prices or threaten future charges.

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Log the portal response payload to see what was returned instead of a url.
  2. Configure the Stripe customer portal (branding, products, links) in the Stripe dashboard — an unconfigured portal can yield sessions without usable URLs.
  3. Fix the server to return an error status instead of 200 when portal creation fails.
  4. Align the client parser with the actual response shape (e.g. payload.session?.url).

Example fix

// before
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.");
// after
const url = isCheckoutUrlResponse(payload) ? payload.url : null;
if (!url) throw new Error(`Billing portal response did not include a URL (keys: ${Object.keys(payload ?? {}).join(",")}).`);
Defensive patterns

Strategy: type-guard

Validate before calling

function hasUrl(p: unknown): p is { url: string } {
  return typeof p === "object" && p !== null && "url" in p && typeof (p as { url: unknown }).url === "string";
}

Type guard

function isPortalUrlResponse(p: unknown): p is { url: string } {
  return typeof p === "object" && p !== null && "url" in p && typeof (p as { url: unknown }).url === "string";
}

Try / catch

try {
  await openStripePortal();
} catch (error) {
  setStripeError(error instanceof Error ? error.message : "Could not open the billing portal.");
}

Prevention

When it happens

Trigger: POST /v1/billing/stripe/portal returns 200 with {}, {url: null}, or a non-string url; the server's Stripe billing portal session creation returned a session object without a URL.

Common situations: Stripe customer portal not configured in the Stripe dashboard (no active portal configuration link); the org has no Stripe customer ID; API response shape changed; keys misconfigured server-side.

Related errors


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