{"record":{"id":"c18624e3378e9012","repo":"different-ai/openwork","slug":"seat-checkout-response-did-not-include-a-url","errorCode":null,"errorMessage":"Seat checkout response did not include a URL.","messagePattern":"Seat checkout 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":295,"sourceCode":"\n  async function startSeatCheckout() {\n    if (!canManageBillingSettings) {\n      setStripeError(\"Admins can start seat checkout from Members. Owners and super-admins manage Billing settings here.\");\n      return;\n    }\n\n    setStripeError(null);\n    try {\n      await runReauthableAction(\"seat-checkout\", async () => {\n        setStripeActionBusy(\"seat-checkout\");\n        const { response, payload } = await requestJson(\n          \"/v1/billing/stripe/checkout\",\n          { method: \"POST\", body: JSON.stringify({ type: \"seat\" }) },\n          12000,\n        );\n        if (!response.ok) throw getRequestError(payload, response, `Seat checkout 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(\"Seat checkout response did not include a URL.\");\n        window.location.href = url;\n      });\n    } catch (error) {\n      setStripeError(error instanceof Error ? error.message : \"Could not start seat billing checkout.\");\n    } 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 () => {","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-web/app/(den)/dashboard/_components/billing-dashboard-screen.tsx#L277-L313","documentation":"startSeatCheckout POSTs {type:\"seat\"} to /v1/billing/stripe/checkout expecting a JSON body with a string 'url' field for the Stripe-hosted checkout session. The error is thrown when the endpoint responds 200 OK but the payload contains no usable url, so the redirect (window.location.href = url) cannot proceed.","triggerScenarios":"POST /v1/billing/stripe/checkout returns 200 with a body like {}, {url: null}, or {url: 123}; the server-side Stripe checkout session creation silently failed but still returned success.","commonSituations":"Stripe publishable/secret keys misconfigured on the Den server; checkout session creation returning a session without a URL; response schema change (url nested under data); org has no Stripe customer yet and server returns an empty success.","solutions":["Log the checkout response payload to confirm what the server actually returned for 'url'.","Fix the server handler to return 4xx/5xx with getRequestError-compatible detail instead of a 200 without a URL.","Verify the server can create Stripe checkout sessions (valid API keys, price configured for seat billing).","Check client and server share the same response contract ({url: string}); update parsing if the field moved (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(\"Seat checkout response did not include a URL.\");\n// after\nconst url = isCheckoutUrlResponse(payload) ? payload.url : null;\nif (!url) throw new Error(`Seat checkout 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}\n// before redirecting: if (!hasUrl(payload)) throw ...","typeGuard":"function isCheckoutUrlResponse(p: unknown): p is { url: string } {\n  return typeof p === \"object\" && p !== null && \"url\" in p && typeof (p as { url: unknown }).url === \"string\" && (p as { url: string }).url.startsWith(\"https://\");\n}","tryCatchPattern":"try {\n  await startSeatCheckout();\n} catch (error) {\n  setStripeError(error instanceof Error ? error.message : \"Could not start seat billing checkout.\");\n}","preventionTips":["Validate checkout responses server-side and return 5xx with detail instead of 200 without a url","Type the endpoint response and parse it with Zod so missing fields fail loudly in dev","Monitor Stripe checkout session creation errors in server logs","Keep a direct Stripe dashboard link as an operator fallback when portal/checkout APIs misbehave"],"tags":["api-response","stripe","checkout","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"}