How Ghost Integrates with Stripe for Subscription Payments

Ghost integrates with Stripe through a modular three-layer architecture consisting of StripeService, StripeAPI, and WebhookManager, enabling secure checkout sessions, webhook handling, and subscription lifecycle management directly from the core server layer.

Ghost's payment engine, located in the TryGhost/Ghost repository, implements a comprehensive Stripe integration that handles everything from initial checkout to subscription renewals. The integration lives in the core server layer and uses the official Stripe Node.js SDK wrapped in domain-specific abstractions. This architecture allows Ghost to process membership payments while keeping the codebase testable and maintainable.

Architecture Overview

The Stripe integration consists of three primary components that work together to orchestrate payment flows.

StripeService

StripeService acts as the high-level orchestrator that coordinates between the Stripe API client, database models, and webhook handling. Instantiated with dependencies including labs, members service, and settings cache, this service exposes methods to connect, configure, disconnect, and perform subscription-related actions. The service is defined in ghost/core/core/server/services/stripe/stripe-service.js.

StripeAPI

StripeAPI provides a thin wrapper around the official Stripe Node SDK. This component creates the Stripe client instance, applies Ghost-specific configuration including API version pinning and rate-limiting buckets, and implements all low-level Stripe API calls. It handles operations for customers, products, prices, checkout sessions, and subscription updates. The implementation resides in ghost/core/core/server/services/stripe/stripe-api.js.

WebhookManager

WebhookManager manages the registration, verification, and parsing of Stripe webhook endpoints. It stores webhook IDs and secrets in the Ghost settings model and supports both "network" mode (creating live endpoints) and "local" mode (using secrets supplied by stripe-cli). The manager defines the specific Stripe events Ghost processes, including checkout completion and subscription lifecycle events. Find it in ghost/core/core/server/services/stripe/webhook-manager.js.

End-to-End Subscription Flow

Understanding how Ghost processes subscription payments requires examining the complete flow from configuration to webhook handling.

Configuration and Setup

When a site administrator connects a Stripe account in the Ghost admin UI, Ghost persists the secret and public keys along with webhook configuration to the settings model. The StripeService.configure() method receives an IStripeServiceConfig object and passes it to StripeAPI.configure(), subsequently starting the webhook manager.

// Simplified configuration flow
await stripeService.configure({
    secretKey: settings.stripe_secret_key,
    publicKey: settings.stripe_publishable_key,
    enablePromoCodes: true,
    enableAutomaticTax: false,
    checkoutSessionSuccessUrl: `${siteUrl}/members/thank-you/`,
    checkoutSessionCancelUrl: `${siteUrl}/members/`,
    webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
    webhookHandlerUrl: `${siteUrl}/members/webhooks/stripe/`,
    siteUrl
});

Creating Checkout Sessions

When a visitor initiates a subscription, Ghost calls StripeAPI.createCheckoutSession(). This method constructs a stripeSessionOptions object that includes payment_method_types, success and cancel URLs, optional promotion codes, automatic tax settings, and a subscription_data block containing trial settings and custom metadata with Ghost attribution data.

// Example from the members service
const session = await stripeService.api.createCheckoutSession(
    priceId,
    memberStripeCustomer,
    {
        successUrl: `${siteUrl}/members/thank-you/`,
        cancelUrl: `${siteUrl}/members/`,
        metadata: { attribution_id: member.id },
        trialDays: 14,
        coupon: 'WELCOME10'
    }
);
// Redirect browser to session.url

Webhook Event Handling

After checkout completion, Stripe sends a checkout.session.completed event to the configured webhook URL. The WebhookManager.start() method registers or updates the endpoint on Stripe and stores the webhook secret. Incoming requests route to StripeService.webhookController, which uses WebhookManager.parseWebhook() to parse events and forward them to specific event services.

// Inside ghost/core/core/server/services/stripe/webhook-controller.js
const event = webhookManager.parseWebhook(body, signature);
switch (event.type) {
    case 'checkout.session.completed':
        await subscriptionEventService.handleCheckoutCompleted(event);
        break;
    case 'invoice.payment_succeeded':
        await invoiceEventService.handlePaymentSucceeded(event);
        break;
}

Subscription Lifecycle Management

Ghost interacts with the Stripe API to manage ongoing subscriptions through several key methods. The StripeAPI.createSubscription() and StripeAPI.updateSubscriptionItemPrice() methods handle creation and plan changes. Cancellation occurs via StripeAPI.cancelSubscription() for immediate termination or StripeAPI.cancelSubscriptionAtPeriodEnd() for end-of-period termination. Coupon management uses StripeAPI.addCouponToSubscription() and StripeAPI.removeCouponFromSubscription().

Billing Portal Integration

Members manage payment methods and view invoices through Stripe's hosted billing portal. Ghost generates portal sessions using StripeAPI.createBillingPortalSession(), passing a return_url that redirects members back to their account page.

async function openBillingPortal(member) {
    const customer = await stripeService.api.getCustomerForMemberCheckoutSession(member);
    const portalSession = await stripeService.api.createBillingPortalSession(customer, {
        returnUrl: `${siteUrl}/members/account/`
    });
    return portalSession.url;
}

Implementation Examples

Initializing the Stripe Service

The StripeService requires dependency injection during instantiation before configuration.

const StripeService = require('./stripe/stripe-service');
const stripeService = new StripeService({
    labs,
    membersService,
    donationService,
    giftService,
    staffService,
    StripeWebhook: require('./models/stripe-webhook'),
    settingsCache,
    models
});

await stripeService.configure({
    secretKey: process.env.STRIPE_SECRET_KEY,
    publicKey: process.env.STRIPE_PUBLISHABLE_KEY,
    enablePromoCodes: true,
    enableAutomaticTax: true,
    checkoutSessionSuccessUrl: `${siteUrl}/members/thank-you/`,
    checkoutSessionCancelUrl: `${siteUrl}/members/`,
    webhookHandlerUrl: `${siteUrl}/members/webhooks/stripe/`,
    siteUrl
});

Handling Webhook Events in Express

The webhook endpoint parses signatures and delegates to the controller.

// Express route for Stripe webhooks
router.post('/members/webhooks/stripe/', async (req, res) => {
    const sig = req.headers['stripe-signature'];
    try {
        const event = stripeService.webhookManager.parseWebhook(req.rawBody, sig);
        await stripeService.webhookController.handle(event);
        res.sendStatus(200);
    } catch (err) {
        console.error('Stripe webhook error', err);
        res.sendStatus(400);
    }
});

Key Source Files

Summary

  • Ghost implements Stripe integration through three modular layers: StripeService (orchestration), StripeAPI (SDK wrapper), and WebhookManager (event handling).
  • Checkout sessions are created via StripeAPI.createCheckoutSession() with support for trials, coupons, automatic tax, and attribution metadata.
  • Webhook events are parsed by WebhookManager.parseWebhook() and routed through StripeService.webhookController to domain-specific event services like SubscriptionEventService.
  • Subscription lifecycle operations include creation, cancellation (immediate or at period-end), and coupon management through dedicated StripeAPI methods.
  • The billing portal is implemented using StripeAPI.createBillingPortalSession() with configurable return URLs.
  • All Stripe configuration persists in Ghost's settings model, enabling dynamic reconfiguration through the admin UI.

Frequently Asked Questions

How does Ghost handle Stripe webhook security?

Ghost uses the WebhookManager class to verify webhook signatures using the secret stored in the settings model. The parseWebhook() method in ghost/core/core/server/services/stripe/webhook-manager.js validates the Stripe-Signature header against the raw request body before processing events. Invalid signatures result in a 400 response, preventing unauthorized access to payment workflows.

Can Ghost process subscription payments without webhooks?

No, Ghost requires webhooks to maintain subscription state consistency. While the initial checkout session creation works via direct API calls, Ghost relies on webhook events like checkout.session.completed and invoice.payment_succeeded to update internal database tables including members_stripe_customers and members_stripe_customers_subscriptions. The StripeService explicitly starts the webhook manager during configuration to ensure event processing.

Where does Ghost store Stripe API keys and webhook secrets?

Ghost stores Stripe configuration in the settings model (ghost/core/core/server/models/setting.js), accessed through the settingsCache dependency. The StripeService retrieves these values during initialization, passing them to StripeAPI.configure() and WebhookManager.start(). This architecture allows administrators to update Stripe credentials through the Ghost admin UI without code changes.

How does Ghost support Stripe test mode?

Ghost detects test mode through the Stripe API keys provided during configuration. The StripeAPI wrapper initializes the Stripe client with the appropriate secret key, and the WebhookManager can operate in "local" mode using secrets supplied by stripe-cli for development environments. The ghost/core/test/utils/stripe-mocker.js utility provides additional mocking capabilities for unit testing payment flows without hitting Stripe's servers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →