Stirling-Tools/Stirling-PDF · error · Error

No billing portal URL returned

Error message

No billing portal URL returned

What it means

Thrown by updateEnterpriseSeats when the manage-billing invocation succeeded (no error) but the returned data has no url field. The function is expected to return a billing-portal URL for the user to confirm seat changes; an absent url means the response is malformed/degenerate even though no error was raised. Distinct from error 176: the call succeeded but the contract was broken.

Source

Thrown at frontend/editor/src/proprietary/services/licenseService.ts:550

    const baseUrl = window.location.origin;
    const returnUrl = `${baseUrl}/settings/adminPlan?seats_updated=true`;

    const { data, error } = await supabase.functions.invoke("manage-billing", {
      body: {
        return_url: returnUrl,
        license_key: licenseKey,
        self_hosted: true,
        new_seat_count: newSeatCount,
      },
    });

    if (error) {
      throw new Error(`Failed to update seat count: ${error.message}`);
    }

    if (!data || !data.url) {
      throw new Error("No billing portal URL returned");
    }

    return data.url;
  },
};

/**
 * Map license type to plan tier
 * @param licenseInfo - Current license information
 * @returns Plan tier: 'free' | 'server' | 'enterprise'
 */
export const mapLicenseToTier = (
  licenseInfo: LicenseInfo | null,
): "free" | "server" | "enterprise" | null => {
  if (!licenseInfo) return null;

  // No license or NORMAL type = Free tier
  if (licenseInfo.licenseType === "NORMAL" || !licenseInfo.enabled) {

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Inspect the actual response body from manage-billing to see which field carries the URL.
  2. Update the edge function to always return {url} on success, or update the client if the field was renamed.
  3. Catch the error and surface a 'could not open billing portal' message with a retry.
  4. Add a server-side log in manage-billing for the no-url branch.

Example fix

// before
if (!data || !data.url) throw new Error("No billing portal URL returned");

// after — tolerate the renamed field, else surface a clear error
const url = data?.url ?? data?.portal_url;
if (!url) throw new Error("No billing portal URL returned");
return url;
Defensive patterns

Strategy: try-catch

Validate before calling

// caller can't pre-validate response shape; ensure edge function returns {url} on success

Try / catch

try {
  const url = await licenseService.updateEnterpriseSeats(newCount, key);
  window.location.href = url;
} catch (e) {
  if (/No billing portal URL/.test(e instanceof Error ? e.message : "")) {
    showToast(t('license.portalUrlMissing'));
  } else throw e;
}

Prevention

When it happens

Trigger: The manage-billing function returned 2xx with an empty/partial body; a refactor changed the response shape (e.g. url renamed to portal_url) and the client hasn't caught up; the function returned early without creating the session; Stripe returned no URL for an edge case the function didn't handle.

Common situations: Edge function version skew with the client; function silently returns {} on an unhandled branch; Stripe API change where the portal session object lacks url.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/a5cb6e3ac7c89c72. Report an issue: GitHub.