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
- Inspect the actual response body from manage-billing to see which field carries the URL.
- Update the edge function to always return {url} on success, or update the client if the field was renamed.
- Catch the error and surface a 'could not open billing portal' message with a retry.
- 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
- Keep the manage-billing response shape under test so url is always returned on success.
- Log the no-url branch server-side for diagnosis.
- Version-skew the client when the edge function's response field is renamed.
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
- Failed to update seat count: ${error.message}
- No pricing data returned
- Supabase is not configured. Seat updates are not available.
- Supabase is not configured. Please use static plans instead.
- Failed to fetch plans: ${error.message}
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/a5cb6e3ac7c89c72.
Report an issue: GitHub.