{"record":{"id":"a84a186148116625","repo":"TryGhost/Ghost","slug":"unable-to-create-stripe-billing-portal-session","errorCode":null,"errorMessage":"Unable to create Stripe billing portal session","messagePattern":"Unable to create Stripe billing portal session","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/portal/src/utils/api.js","lineNumber":797,"sourceCode":"                const returnUrlObj = new URL(siteUrl);\n                returnUrlObj.searchParams.set('stripe', 'billing-portal-closed');\n                returnUrl = returnUrlObj.href;\n            }\n\n            return makeRequest({\n                url,\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify({\n                    identity: identity,\n                    subscription_id: subscriptionId,\n                    returnUrl\n                })\n            }).then(function (res) {\n                if (!res.ok) {\n                    throw new Error('Unable to create Stripe billing portal session');\n                }\n                return res.json();\n            }).then(function (result) {\n                return window.location.assign(result.url);\n            }).catch(function (err) {\n                throw err;\n            });\n        },\n\n        async updateSubscription({subscriptionId, tierId, cadence, planId, smartCancel, cancelAtPeriodEnd, cancellationReason}) {\n            const identity = await api.member.identity();\n            const url = endpointFor({type: 'members', resource: 'subscriptions'}) + subscriptionId + '/';\n            const body = {\n                smart_cancel: smartCancel,\n                cancel_at_period_end: cancelAtPeriodEnd,\n                cancellation_reason: cancellationReason,\n                identity: identity,\n                priceId: planId","sourceCodeStart":779,"sourceCodeEnd":815,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/portal/src/utils/api.js#L779-L815","documentation":"Thrown by manageBilling() when the POST to members 'create-stripe-billing-portal-session' returns non-2xx. This endpoint creates a Stripe Customer Portal session for the member to manage their subscription (cancel, swap tiers, view invoices). Like editBilling, this throw discards the backend error body and always uses the generic message.","triggerScenarios":"Member opens the billing portal link; backend can't create a Customer Portal session — the member has no Stripe customer record, the subscriptionId doesn't exist or belongs to another member, Stripe Customer Portal isn't configured in the Stripe dashboard, or identity is stale.","commonSituations":"Stripe Customer Portal not enabled/configured in the Stripe account (admin must enable it in Stripe → Settings → Billing → Customer portal); member only has a complimentary (Ghost-managed) subscription with no Stripe customer; subscriptionId mismatch; Stripe Connect key issue.","solutions":["Ensure the Stripe Customer Portal is enabled and configured in the Stripe dashboard (a prerequisite Ghost can't set for you).","Confirm the member has a real Stripe subscription (not a complimentary one).","Check the Ghost server log for the create-stripe-billing-portal-session error reason.","Re-authenticate the member and pass a valid subscriptionId.","Patch manageBilling() to surface the backend res.json() error body for better triage."],"exampleFix":"// before: backend reason discarded\nif (!res.ok) {\n    throw new Error('Unable to create Stripe billing portal session');\n}\n\n// after: surface the backend error\nif (!res.ok) {\n    const errData = await res.json().catch(() => null);\n    throw new Error(errData?.errors?.[0]?.message || 'Unable to create Stripe billing portal session');\n}","handlingStrategy":"try-catch","validationCode":"// Confirm the Stripe Customer Portal is configured before offering the link\n// (this is a Stripe-dashboard prerequisite, not a Ghost setting)\n// Then validate the subscription context:\nfunction canManageBilling(identity, subscriptionId) {\n    return Boolean(identity && subscriptionId);\n}","typeGuard":null,"tryCatchPattern":"try {\n    await api.member.manageBilling({subscriptionId});\n} catch (err) {\n    // Generic message — server log has the reason (often 'portal not configured')\n    showToast(err.message);\n}","preventionTips":["Enable and configure the Stripe Customer Portal in the Stripe dashboard before exposing the link.","Only offer the billing portal to members with a real Stripe subscription.","Patch manageBilling to surface the backend error body for diagnosability."],"tags":["portal","stripe","billing-portal","subscription","members-api"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}