Ghost's native membership and subscription system integrates with Stripe for payments. Track the complete subscription lifecycle with GA4 e-commerce events to measure revenue, conversion rates, and member value.
Ghost Membership Model
Ghost memberships follow an e-commerce pattern:
- Products - Ghost Tiers (Free, Paid, Custom tiers)
- Transactions - Member subscriptions (monthly/yearly)
- Checkout - Ghost Portal payment flow
- Revenue - Stripe-processed subscription payments
- Customers - Ghost Members with paid subscriptions
E-commerce Event Mapping
Map Ghost membership actions to GA4 e-commerce events:
| Ghost Action | GA4 Event | Description |
|---|---|---|
| View pricing | view_item_list |
Member views available tiers |
| Select tier | select_item |
Member clicks on tier |
| Begin checkout | begin_checkout |
Portal checkout opens |
| Add payment info | add_payment_info |
Stripe payment form shown |
| Purchase subscription | purchase |
Successful subscription |
| Upgrade tier | purchase |
Tier upgrade completed |
| Downgrade tier | refund |
Tier downgrade (partial refund model) |
| Cancel subscription | refund |
Subscription cancelled |
Implementation Methods
Method 1: Ghost Portal Events (Recommended)
Track subscription events using Ghost Portal's custom events:
{{!-- In default.hbs, before </body> --}}
<script>
// Track tier selection in Portal
window.addEventListener('portal-open', function(event) {
if (typeof gtag !== 'undefined') {
// User opened portal - track tier view
gtag('event', 'view_item_list', {
item_list_id: 'ghost_tiers',
item_list_name: 'Membership Tiers',
items: [
{{#foreach @site.members_tiers}}
{
item_id: '{{id}}',
item_name: '{{name}}',
item_category: 'Ghost Membership',
price: {{monthly_price}},
currency: '{{currency}}',
{{#if description}}
item_variant: '{{description}}',
{{/if}}
}{{#unless @last}},{{/unless}}
{{/foreach}}
]
});
}
});
// Track checkout initiation
window.addEventListener('portal-checkout-start', function(event) {
if (typeof gtag !== 'undefined') {
gtag('event', 'begin_checkout', {
event_category: 'ecommerce',
event_label: 'ghost_subscription_checkout',
currency: 'USD', // Update with your currency
value: 0, // Will be updated when tier is selected
items: []
});
}
});
// Track successful subscription
window.addEventListener('portal-subscription-created', function(event) {
if (typeof gtag !== 'undefined') {
{{#if @member.subscriptions}}
var subscription = {{json @member.subscriptions.[0]}};
var tier = subscription.tier;
gtag('event', 'purchase', {
transaction_id: subscription.id,
value: tier.monthly_price || tier.yearly_price,
currency: tier.currency || 'USD',
items: [{
item_id: tier.id,
item_name: tier.name,
item_category: 'Ghost Membership',
item_variant: subscription.cadence, // monthly or yearly
price: subscription.cadence === 'month' ? tier.monthly_price : tier.yearly_price,
quantity: 1
}]
});
{{/if}}
}
});
// Track subscription cancellation
window.addEventListener('portal-subscription-cancelled', function(event) {
if (typeof gtag !== 'undefined') {
gtag('event', 'refund', {
event_category: 'ecommerce',
event_label: 'subscription_cancelled',
currency: 'USD',
value: 0, // Can't track refund amount from portal event
transaction_id: event.detail?.subscription_id || 'unknown'
});
}
});
</script>
Note: Ghost Portal events have limited data access. For complete e-commerce tracking, use Method 2.
Method 2: Stripe Webhook Integration (Server-Side)
For accurate revenue tracking, use Stripe webhooks with Ghost Admin API:
Step 1: Create Webhook Handler
Create a server-side webhook endpoint (Node.js example):
// webhook-handler.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const axios = require('axios');
const app = express();
app.post('/webhook/stripe', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle subscription events
switch (event.type) {
case 'customer.subscription.created':
await trackSubscriptionPurchase(event.data.object);
break;
case 'customer.subscription.updated':
await trackSubscriptionUpdate(event.data.object);
break;
case 'customer.subscription.deleted':
await trackSubscriptionCancellation(event.data.object);
break;
case 'invoice.payment_succeeded':
await trackPayment(event.data.object);
break;
}
res.json({received: true});
});
async function trackSubscriptionPurchase(subscription) {
// Send to GA4 Measurement Protocol
const measurementId = 'G-XXXXXXXXXX';
const apiSecret = 'your-measurement-protocol-secret';
await axios.post(
`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
{
client_id: subscription.metadata.ghost_member_uuid || subscription.customer,
events: [{
name: 'purchase',
params: {
transaction_id: subscription.id,
value: (subscription.items.data[0].price.unit_amount / 100),
currency: subscription.currency.toUpperCase(),
items: [{
item_id: subscription.items.data[0].price.product,
item_name: subscription.metadata.tier_name || 'Paid Membership',
item_category: 'Ghost Membership',
item_variant: subscription.items.data[0].price.recurring.interval,
price: (subscription.items.data[0].price.unit_amount / 100),
quantity: 1
}]
}
}]
}
);
}
async function trackPayment(invoice) {
// Track recurring payments
if (invoice.billing_reason === 'subscription_cycle') {
const measurementId = 'G-XXXXXXXXXX';
const apiSecret = 'your-measurement-protocol-secret';
await axios.post(
`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
{
client_id: invoice.customer,
events: [{
name: 'recurring_payment',
params: {
transaction_id: invoice.id,
value: (invoice.amount_paid / 100),
currency: invoice.currency.toUpperCase(),
subscription_id: invoice.subscription,
payment_type: 'recurring'
}
}]
}
);
}
}
async function trackSubscriptionCancellation(subscription) {
const measurementId = 'G-XXXXXXXXXX';
const apiSecret = 'your-measurement-protocol-secret';
await axios.post(
`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
{
client_id: subscription.metadata.ghost_member_uuid || subscription.customer,
events: [{
name: 'refund',
params: {
transaction_id: subscription.id,
event_category: 'subscription',
event_label: 'cancelled',
cancellation_reason: subscription.cancellation_details?.reason || 'unknown'
}
}]
}
);
}
app.listen(3000);
Step 2: Configure Stripe Webhook
- Log in to Stripe Dashboard
- Navigate to Developers → Webhooks
- Click Add endpoint
- Enter your webhook URL:
https://yourdomain.com/webhook/stripe - Select events:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeeded
Step 3: Store Member UUID in Stripe
Modify Ghost theme to pass member UUID to Stripe:
<script>
window.addEventListener('portal-checkout-start', function() {
{{#if @member}}
// Add member UUID to Stripe metadata
if (window.Stripe) {
// This requires custom Ghost Portal modification
console.log('Member UUID:', '{{@member.uuid}}');
}
{{/if}}
});
</script>
Method 3: Client-Side Checkout Tracking
Track checkout steps in Ghost theme:
{{!-- In page.hbs or post.hbs where pricing is shown --}}
<script>
{{#if @site.members_enabled}}
// Track tier view
if (typeof gtag !== 'undefined') {
gtag('event', 'view_item_list', {
item_list_id: 'membership_tiers',
item_list_name: 'Ghost Membership Plans',
items: [
{{#foreach @site.members_tiers}}
{
item_id: '{{id}}',
item_name: '{{name}}',
item_category: 'Membership',
{{#if monthly_price}}
price: {{monthly_price}},
{{/if}}
{{#if currency}}
currency: '{{currency}}',
{{/if}}
item_variant: 'monthly'
}{{#unless @last}},{{/unless}}
{{/foreach}}
]
});
}
// Track tier selection (when user clicks pricing button)
document.querySelectorAll('.tier-select-button').forEach(function(button) {
button.addEventListener('click', function() {
var tierId = this.dataset.tierId;
var tierName = this.dataset.tierName;
var tierPrice = parseFloat(this.dataset.tierPrice);
if (typeof gtag !== 'undefined') {
gtag('event', 'select_item', {
item_list_id: 'membership_tiers',
item_list_name: 'Ghost Membership Plans',
items: [{
item_id: tierId,
item_name: tierName,
item_category: 'Membership',
price: tierPrice
}]
});
// Track begin_checkout
gtag('event', 'begin_checkout', {
currency: 'USD',
value: tierPrice,
items: [{
item_id: tierId,
item_name: tierName,
item_category: 'Membership',
price: tierPrice,
quantity: 1
}]
});
}
});
});
{{/if}}
</script>
Enhanced E-commerce Tracking
Track Free Tier Signups
Free memberships are still conversions:
<script>
window.addEventListener('portal-signup', function(event) {
{{#if @member}}
{{#unless @member.paid}}
// Free member signup
if (typeof gtag !== 'undefined') {
gtag('event', 'purchase', {
transaction_id: '{{@member.uuid}}',
value: 0,
currency: 'USD',
items: [{
item_id: 'free-tier',
item_name: 'Free Membership',
item_category: 'Membership',
price: 0,
quantity: 1
}]
});
}
{{/unless}}
{{/if}}
});
</script>
Track Tier Upgrades
Monitor when free members upgrade to paid:
<script>
window.addEventListener('portal-subscription-created', function(event) {
{{#if @member}}
var wasFree = !{{@member.paid}};
if (wasFree && typeof gtag !== 'undefined') {
// This is an upgrade from free to paid
gtag('event', 'tier_upgrade', {
event_category: 'membership',
event_label: 'free_to_paid',
{{#if @member.subscriptions}}
new_tier: '{{@member.subscriptions.[0].tier.name}}',
subscription_value: {{@member.subscriptions.[0].tier.monthly_price}}
{{/if}}
});
}
{{/if}}
});
</script>
Track Subscription Lifetime Value
Calculate and send LTV metrics:
<script>
{{#if @member.subscriptions}}
var subscription = {{json @member.subscriptions.[0]}};
var monthlyPrice = subscription.tier.monthly_price || 0;
var yearlyPrice = subscription.tier.yearly_price || 0;
var createdDate = new Date('{{@member.created_at}}');
var now = new Date();
var monthsSubscribed = (now - createdDate) / (1000 * 60 * 60 * 24 * 30);
var lifetimeValue = monthlyPrice * monthsSubscribed;
if (typeof gtag !== 'undefined') {
gtag('set', 'user_properties', {
'member_ltv': lifetimeValue.toFixed(2),
'subscription_age_months': Math.floor(monthsSubscribed),
'subscription_tier': subscription.tier.name
});
}
{{/if}}
</script>
GA4 E-commerce Configuration
Enable E-commerce Reporting
- Navigate to GA4 → Admin → Data Streams
- Select your web data stream
- Click Configure tag settings → Show more
- Enable Enhanced measurement
- Turn on E-commerce events
Create Custom E-commerce Reports
Admin → Custom Definitions → Custom Metrics:
| Metric Name | Parameter | Scope |
|---|---|---|
| Subscription Value | value | Event |
| Monthly Price | price | Event |
| Subscription Age | subscription_age_months | User |
| Member LTV | member_ltv | User |
Admin → Custom Definitions → Custom Dimensions:
| Dimension Name | Parameter | Scope |
|---|---|---|
| Subscription Tier | item_name | Event |
| Billing Cadence | item_variant | Event |
| Member Status | member_status | User |
| Cancellation Reason | cancellation_reason | Event |
Create E-commerce Funnel
Explore → Funnel Exploration:
- Tier View -
view_item_list - Tier Selected -
select_item - Checkout Started -
begin_checkout - Payment Info Added -
add_payment_info - Purchase Completed -
purchase
Performance Tracking
Key Metrics to Monitor
- Conversion Rate - Portal views → Subscriptions
- Average Order Value (AOV) - Average subscription value
- Subscriber Lifetime Value (LTV) - Total revenue per subscriber
- Churn Rate - Subscription cancellations / total subscriptions
- Upgrade Rate - Free → Paid conversions
- Revenue by Tier - Revenue breakdown by membership tier
Create LTV Cohort Analysis
<script>
{{#if @member}}
var joinedDate = new Date('{{@member.created_at}}');
var cohortMonth = joinedDate.getFullYear() + '-' + String(joinedDate.getMonth() + 1).padStart(2, '0');
if (typeof gtag !== 'undefined') {
gtag('set', 'user_properties', {
'cohort_month': cohortMonth,
'days_since_join': Math.floor((new Date() - joinedDate) / (1000 * 60 * 60 * 24))
});
}
{{/if}}
</script>
Common Issues
Revenue Mismatch with Stripe
- Currency Conversion - GA4 uses single currency; convert Stripe amounts
- Refunds Not Tracked - Implement webhook-based refund tracking
- Recurring Payments - Track each renewal, not just initial purchase
- Failed Payments - Don't track failed Stripe charges
Duplicate Purchase Events
- Portal + Webhook - Use only one tracking method (webhook preferred)
- Client-Side Issues - Portal events can fire multiple times
- Use Transaction IDs - GA4 deduplicates by
transaction_id
Missing Member Context
- Cached Pages - Member data may be stale on cached pages
- Use Server-Side - Webhook tracking more accurate than client-side
- UUID Issues - Ensure member UUID passed to all events
Attribution Problems
- Multi-Touch - Members may sign up via multiple channels
- Cookie Consent - Ensure tracking runs before checkout
- Cross-Device - Track member UUID for cross-device attribution
Testing E-commerce Tracking
Test Purchase Flow
- Enable GA4 DebugView (Admin → DebugView)
- Open Ghost site in incognito
- Click membership tier button
- Complete checkout flow (use Stripe test mode)
- Verify events in DebugView:
view_item_listselect_itembegin_checkoutpurchase
Validate Revenue
Compare GA4 revenue with Stripe:
// In GA4 Explore → Free Form
// Metric: Total Revenue
// Dimension: Date
// Compare with Stripe Dashboard → Payments
Next Steps
- GTM Setup - Centralize tracking with Tag Manager
- GTM Data Layer - Enhanced e-commerce via GTM
- Troubleshooting Events - Debug tracking issues