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
- Log the portal response payload to see what was returned instead of a url.
- Configure the Stripe customer portal (branding, products, links) in the Stripe dashboard — an unconfigured portal can yield sessions without usable URLs.
- Fix the server to return an error status instead of 200 when portal creation fails.
- 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
- Complete the Stripe customer portal configuration in the Stripe dashboard before enabling the button
- Ensure every billing org has a Stripe customer ID before offering portal access
- Zod-parse the portal response on the client so contract drift is caught in tests
- Return proper HTTP error codes from the server when portal session creation fails
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
- Billing response was incomplete.
- Seat checkout response did not include a URL.
- Invalid cloud provider sync response.
- Invalid cloud provider sync status.
- Invalid cloud provider sync status response.
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/36b3b2413d3c87cc.
Report an issue: GitHub.