{"record":{"id":"36b3b2413d3c87cc","repo":"different-ai/openwork","slug":"billing-portal-response-did-not-include-a-url","errorCode":null,"errorMessage":"Billing portal response did not include a URL.","messagePattern":"Billing portal response did not include a URL\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx","lineNumber":318,"sourceCode":"    } finally {\n      setStripeActionBusy(null);\n    }\n  }\n\n  async function openStripePortal() {\n    if (!canManageBillingSettings) {\n      setStripeError(\"Only workspace owners and super-admins can open billing portals from Settings.\");\n      return;\n    }\n\n    setStripeError(null);\n    try {\n      await runReauthableAction(\"billing-portal\", async () => {\n        setStripeActionBusy(\"portal\");\n        const { response, payload } = await requestJson(\"/v1/billing/stripe/portal\", { method: \"POST\" }, 12000);\n        if (!response.ok) throw getRequestError(payload, response, `Billing portal failed (${response.status}).`);\n        const url = payload && typeof payload === \"object\" && \"url\" in payload && typeof payload.url === \"string\" ? payload.url : null;\n        if (!url) throw new Error(\"Billing portal response did not include a URL.\");\n        window.location.href = url;\n      });\n    } catch (error) {\n      setStripeError(error instanceof Error ? error.message : \"Could not open the billing portal.\");\n    } finally {\n      setStripeActionBusy(null);\n    }\n  }\n\n  const showPolar = polarBilling?.hasActivePlan === true && Boolean(polarBilling.portalUrl);\n  const stripePrice = stripeBilling ? formatMoneyMinor(stripeBilling.unitAmount, stripeBilling.currency) : null;\n  const seatBilling = stripeBilling?.seats;\n  const webBilling = stripeBilling?.web ?? null;\n  const seatPrice = seatBilling ? formatMoneyMinor(seatBilling.unitAmount, seatBilling.currency) : null;\n  const activeMemberCount = stripeBilling?.memberCount ?? 0;\n\n  // Without Stripe keys the deployment never charges for either product, so the\n  // page must not quote prices or threaten future charges.","sourceCodeStart":300,"sourceCodeEnd":336,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx#L300-L336","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nconst url = payload && typeof payload === \"object\" && \"url\" in payload && typeof payload.url === \"string\" ? payload.url : null;\nif (!url) throw new Error(\"Billing portal response did not include a URL.\");\n// after\nconst url = isCheckoutUrlResponse(payload) ? payload.url : null;\nif (!url) throw new Error(`Billing portal response did not include a URL (keys: ${Object.keys(payload ?? {}).join(\",\")}).`);","handlingStrategy":"type-guard","validationCode":"function hasUrl(p: unknown): p is { url: string } {\n  return typeof p === \"object\" && p !== null && \"url\" in p && typeof (p as { url: unknown }).url === \"string\";\n}","typeGuard":"function isPortalUrlResponse(p: unknown): p is { url: string } {\n  return typeof p === \"object\" && p !== null && \"url\" in p && typeof (p as { url: unknown }).url === \"string\";\n}","tryCatchPattern":"try {\n  await openStripePortal();\n} catch (error) {\n  setStripeError(error instanceof Error ? error.message : \"Could not open the billing portal.\");\n}","preventionTips":["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"],"tags":["api-response","stripe","billing-portal","validation"],"backgroundTag":"missing-response-field","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}