Courses/Stripe Payments & Subscriptions
Standalone Course~30 min12 challenges

Stripe Payments & Subscriptions for SaaS

Every SaaS needs payments. Stripe is the industry standard — it handles credit cards, subscriptions, invoices, and compliance so you don't have to. In this course, you'll integrate Stripe into your Grit app: create checkout sessions, manage subscription plans, handle webhooks, and build a complete billing system with a customer portal.


What is Stripe?

Accepting payments on the internet is surprisingly complex. You need to securely collect credit card numbers, comply with PCI-DSS regulations, handle failed charges, manage refunds, send invoices, and deal with subscriptions. Stripe does all of this for you. You never see or store a credit card number — Stripe handles the sensitive parts.

Stripe: A payment processing platform that handles credit card payments, subscriptions, invoices, and financial compliance. Instead of building payment infrastructure yourself (which requires PCI-DSS certification), you integrate Stripe's API. Stripe takes a fee per transaction (typically 2.9% + 30 cents) and handles everything else.
Payment Gateway: The intermediary between your application and the credit card networks (Visa, Mastercard, etc.). When a customer enters their card, the payment gateway encrypts it, sends it to the card network, gets approval or rejection, and returns the result to your app. Stripe is both a payment gateway and a full payment platform.
Subscription Billing: Automatically charging a customer on a recurring schedule (monthly, yearly). Stripe manages the entire lifecycle: initial charge, recurring billing, failed payment retries, plan upgrades and downgrades, cancellations, and prorated refunds. You define the plans, Stripe collects the money.

The payment flow with Stripe:

Payment Flow
1. User clicks "Subscribe to Pro Plan" on your site
2. Your API creates a Stripe Checkout Session
3. User is redirected to Stripe's hosted payment page
4. User enters credit card on Stripe's page (you never see the card)
5. Stripe charges the card
6. Stripe redirects user back to your success URL
7. Stripe sends a webhook to your API: "payment succeeded"
8. Your API activates the user's subscription
You never handle credit card numbers. The user enters their card on Stripe's hosted page, not on your site. This means you don't need PCI-DSS certification (a complex, expensive security audit). Stripe is PCI-DSS Level 1 certified — the highest level.
1

Challenge: Create a Stripe Account

Go to stripe.com and create a free account. Once logged in, find the Dashboard. Switch to "Test Mode" (toggle in the top bar). Find your test API keys under Developers → API Keys. You should see a Publishable key (starts with pk_test_) and a Secret key (starts with sk_test_). Never share the secret key.

The grit-stripe Plugin

The grit-stripe plugin wraps Stripe's Go SDK with opinionated helpers designed for SaaS applications. It provides checkout session creation, subscription management, customer portal access, and webhook handling — all pre-configured for the common SaaS billing patterns.

Terminal
# Install the plugin
go get github.com/MUKE-coder/grit-plugins/grit-stripe

What the plugin provides:

  • CreateCheckoutSession — Redirect users to Stripe's hosted payment page
  • CreatePortalSession — Let users manage their subscription (change plan, update card, cancel)
  • HandleWebhook — Process Stripe events (payment succeeded, subscription cancelled, etc.)
  • GetSubscription — Check a customer's current subscription status
  • CancelSubscription — Cancel at period end or immediately
2

Challenge: Install the Plugin

Install the grit-stripe plugin in your Grit project by runninggo get github.com/MUKE-coder/grit-plugins/grit-stripe from yourapps/api/ directory. Verify it appears in your go.mod andgo.sum files.

Configuration

Stripe uses API keys for authentication. You need three keys in your .env file: the secret key (for server-side API calls), the publishable key (for client-side Stripe.js), and the webhook secret (for verifying webhook signatures).

.env
# Stripe Configuration (TEST MODE — safe for development)
STRIPE_SECRET_KEY=sk_test_51...your_secret_key
STRIPE_PUBLISHABLE_KEY=pk_test_51...your_publishable_key
STRIPE_WEBHOOK_SECRET=whsec_...your_webhook_secret

Initialize the plugin in your API:

internal/config/stripe.go
import gritstripe "github.com/MUKE-coder/grit-plugins/grit-stripe"

func InitStripe() {
    gritstripe.Init(gritstripe.Config{
        SecretKey:      os.Getenv("STRIPE_SECRET_KEY"),
        WebhookSecret:  os.Getenv("STRIPE_WEBHOOK_SECRET"),
    })
}
Stripe test keys (starting with sk_test_ and pk_test_) work with test credit card numbers. Use 4242 4242 4242 4242 with any future expiry and any CVC to simulate a successful payment. Use 4000 0000 0000 0002 to simulate a declined card. No real money is charged in test mode.
3

Challenge: Configure Stripe Keys

Add your Stripe test keys to your .env file. Initialize the grit-stripe plugin in your config. Start your API and verify there are no errors related to Stripe initialization. The secret key starts with sk_test_ — if it starts withsk_live_, you're using production keys (switch to test mode in the Stripe Dashboard).

Creating a Checkout Session

When a user clicks "Subscribe" on your pricing page, your API creates a Checkout Session — a temporary URL that takes the user to Stripe's hosted payment page. The user enters their card on Stripe's page, and Stripe redirects them back to your app after payment.

Checkout Session: A Stripe-hosted payment page created on-demand for each purchase. You specify what the customer is buying (a price/product), where to redirect after success or cancellation, and optionally the customer's email. Stripe handles the entire payment form, card validation, 3D Secure authentication, and error handling. The session expires after 24 hours if unused.
internal/handler/billing.go
import gritstripe "github.com/MUKE-coder/grit-plugins/grit-stripe"

func (h *Handler) CreateCheckout(c *gin.Context) {
    user := middleware.GetUser(c) // From auth middleware

    var input struct {
        PriceID string // json:"price_id"
    }
    c.ShouldBindJSON(&input)

    session := gritstripe.CreateCheckoutSession(gritstripe.CheckoutParams{
        PriceID:       input.PriceID,
        SuccessURL:    "https://myapp.com/billing/success",
        CancelURL:     "https://myapp.com/billing",
        CustomerEmail: user.Email,
        Metadata: map[string]string{
            "user_id": fmt.Sprint(user.ID),
        },
    })

    // Return the Stripe-hosted payment page URL
    c.JSON(200, gin.H{"url": session.URL})
}

On the frontend, when the user clicks a plan's "Subscribe" button:

Frontend Flow
// 1. Call your API to create a checkout session
// POST /api/billing/checkout with { price_id: "price_xxx" }

// 2. API returns { url: "https://checkout.stripe.com/c/pay/..." }

// 3. Redirect the user to Stripe's payment page
// window.location.href = response.url

// 4. User enters card on Stripe's page
// 5. On success: Stripe redirects to your success URL
// 6. On cancel: Stripe redirects to your cancel URL
4

Challenge: Create a Checkout Session

Add a POST /api/billing/checkout endpoint to your API. It should accept a price_id in the request body and return a Stripe Checkout URL. Test it with a tool like curl or Postman. Copy the returned URL and open it in your browser — do you see Stripe's payment page? Use test card 4242 4242 4242 4242 to complete payment.

Subscription Plans

Before you can charge customers, you need to create Products and Prices in your Stripe Dashboard. A Product represents what you're selling (e.g., "Pro Plan"). A Price represents how much it costs and how often (e.g., "$20/month").

A typical SaaS has 3 tiers:

Subscription Tiers
Free Plan
  - Price: $0/month (no Stripe integration needed)
  - Features: 1 project, 100 API calls/day, community support
  - Stripe Price ID: none (default for all users)

Pro Plan
  - Price: $20/month
  - Features: 10 projects, 10,000 API calls/day, email support
  - Stripe Price ID: price_pro_monthly

Team Plan
  - Price: $50/month
  - Features: Unlimited projects, 100,000 API calls/day, priority support, team members
  - Stripe Price ID: price_team_monthly

Create these in the Stripe Dashboard:

  1. 1.Go to Products in the Stripe Dashboard
  2. 2.Click "Add Product"
  3. 3.Name: "Pro Plan", Pricing: Recurring, Amount: $20.00, Billing period: Monthly
  4. 4.Save — Stripe generates a Price ID (starts with price_)
  5. 5.Repeat for Team Plan ($50/month)
Store Price IDs in your .env file or a config table, not hardcoded in your Go code. This way you can change plans or prices without redeploying. You can also add yearly prices (e.g., $200/year for Pro) and let users choose monthly or yearly billing.
5

Challenge: Create Stripe Products

In your Stripe Dashboard (test mode), create 2 products: "Pro Plan" at $20/month and"Team Plan" at $50/month. Copy the Price IDs (they start with price_). Add them to your .env file as STRIPE_PRO_PRICE_ID andSTRIPE_TEAM_PRICE_ID.

Webhook Handling

Here's the problem: the user pays on Stripe's page and gets redirected to your success URL. But what if they close the browser before the redirect? What if the network drops? You can't rely on the redirect to activate their subscription. Instead, Stripe sends webhooks — HTTP POST requests to your API — for every important event.

Webhook: An HTTP POST request that Stripe sends to your API when something happens. Instead of your API polling Stripe ("did the payment go through yet?"), Stripe tells your API ("payment succeeded!"). Webhooks are signed with a secret so you can verify they actually came from Stripe and not an attacker.
internal/handler/webhook.go
func (h *Handler) HandleStripeWebhook(c *gin.Context) {
    gritstripe.HandleWebhook(c, gritstripe.WebhookHandlers{

        // Payment successful — activate subscription
        "checkout.session.completed": func(event stripe.Event) {
            // Extract user_id from session metadata
            // Update user's plan in database
            // Set subscription_status = "active"
            // Set stripe_customer_id on user record
        },

        // Subscription renewed (monthly payment)
        "invoice.payment_succeeded": func(event stripe.Event) {
            // Update subscription period end date
            // Log the payment
        },

        // Payment failed (card declined, expired, etc.)
        "invoice.payment_failed": func(event stripe.Event) {
            // Send email: "Your payment failed"
            // Grace period: keep subscription active for 3 days
            // After grace period: downgrade to free
        },

        // User cancelled — subscription ends at period end
        "customer.subscription.deleted": func(event stripe.Event) {
            // Downgrade user to free plan
            // Set subscription_status = "cancelled"
        },

        // Subscription updated (plan change)
        "customer.subscription.updated": func(event stripe.Event) {
            // Update user's plan in database
            // If upgraded: activate new features immediately
            // If downgraded: keep current plan until period end
        },
    })
}

Register the webhook route (no auth middleware — Stripe needs to access it):

routes.go
// Webhook endpoint — no auth middleware!
// Stripe authenticates via webhook signature, not JWT
r.POST("/api/webhooks/stripe", h.HandleStripeWebhook)
The webhook endpoint must NOT have auth middleware. Stripe sends the request, not a logged-in user. Authentication is handled by the webhook signature — Stripe signs each request with your STRIPE_WEBHOOK_SECRET, and the plugin verifies the signature automatically.
6

Challenge: List Important Stripe Events

For a SaaS application, list 5 Stripe webhook events you should handle and describe what your API should do for each one. Think about: successful payments, failed payments, cancellations, plan changes, and trial expirations. Which event is the most critical and why?

Customer Portal

Users need to manage their subscription: update their credit card, change plans, view invoices, or cancel. Building all of this yourself is weeks of work. Stripe's Customer Portal gives you a hosted page where users can do all of this — with zero UI code from you.

internal/handler/billing.go
func (h *Handler) CreatePortalSession(c *gin.Context) {
    user := middleware.GetUser(c)

    // Get the user's Stripe customer ID from your database
    stripeCustomerID := user.StripeCustomerID

    portalSession := gritstripe.CreatePortalSession(
        stripeCustomerID,
        "https://myapp.com/billing", // Return URL after portal
    )

    c.JSON(200, gin.H{"url": portalSession.URL})
}

// Route: POST /api/billing/portal

The Customer Portal lets users:

  • Update payment method — change credit card, add backup card
  • View invoices — download PDF invoices for each billing period
  • Change plan — upgrade or downgrade (with prorated billing)
  • Cancel subscription — cancel at end of billing period
You need to configure the Customer Portal in the Stripe Dashboard under Settings → Customer Portal. Enable the features you want (plan changes, cancellation, invoice history). You can also customize the branding to match your app.
7

Challenge: Create a Portal Session

Add a POST /api/billing/portal endpoint. After completing a test checkout (from challenge 4), use the Stripe customer ID to create a portal session. Open the returned URL. Can you see the subscription? Can you update the payment method? Can you cancel?

Building a Pricing Page

The pricing page is where users decide which plan to buy. It typically shows 3 plan cards side by side, with features listed for each plan and a "Subscribe" button that creates a checkout session.

Pricing Page Structure
// PricingPage component
//
// Layout: 3 cards in a row (responsive: stack on mobile)
//
// Free Plan Card:
//   - Title: "Free"
//   - Price: "$0/month"
//   - Features: checkmark list (1 project, 100 API calls, etc.)
//   - Button: "Get Started" → link to /register
//
// Pro Plan Card (highlighted — "Most Popular"):
//   - Title: "Pro"
//   - Price: "$20/month"
//   - Features: checkmark list (10 projects, 10K API calls, etc.)
//   - Button: "Subscribe" → POST /api/billing/checkout
//     with price_id = STRIPE_PRO_PRICE_ID
//
// Team Plan Card:
//   - Title: "Team"
//   - Price: "$50/month"
//   - Features: checkmark list (unlimited, priority support, etc.)
//   - Button: "Subscribe" → POST /api/billing/checkout
//     with price_id = STRIPE_TEAM_PRICE_ID
//
// Each Subscribe button:
//   1. Calls POST /api/billing/checkout
//   2. Gets back { url: "https://checkout.stripe.com/..." }
//   3. Redirects: window.location.href = url

Design tips for an effective pricing page:

  • Highlight the recommended plan with a border, badge ("Most Popular"), or different background
  • Show savings for yearly billing — add a toggle (Monthly/Yearly) with a "Save 20%" badge on yearly
  • Use checkmarks for features — green checkmarks for included, gray X or dash for excluded
  • Keep it simple — 3 plans maximum, clear differentiation between each tier
8

Challenge: Build a Pricing Page

Build a pricing page with 3 plan cards: Free ($0), Pro ($20/mo), and Team ($50/mo). Each paid plan's button should call your checkout endpoint with the correct Price ID and redirect to Stripe. Style the Pro plan as the recommended option. Add at least 4 features per plan with checkmark icons.

Checking Subscription Status

Once users can subscribe, you need to check their subscription status on protected routes. Pro features should only be accessible to Pro subscribers. Team features should only be accessible to Team subscribers.

Store subscription data on the User model:

models/user.go
type User struct {
    gorm.Model
    Name               string
    Email              string
    Password           string
    Role               string // "USER", "ADMIN"

    // Stripe fields
    StripeCustomerID   string // Stripe customer ID
    SubscriptionPlan   string // "free", "pro", "team"
    SubscriptionStatus string // "active", "cancelled", "past_due"
    PeriodEnd          time.Time // When current billing period ends
}
middleware/subscription.go
func RequireSubscription(plans ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        user := GetUser(c)

        // Check if user's plan is in the required plans
        allowed := false
        for _, plan := range plans {
            if user.SubscriptionPlan == plan {
                allowed = true
                break
            }
        }

        if !allowed {
            c.JSON(403, gin.H{
                "error": "This feature requires a " + plans[0] + " subscription",
            })
            c.Abort()
            return
        }

        c.Next()
    }
}

// Usage in routes:
// r.GET("/api/analytics", RequireSubscription("pro", "team"), h.GetAnalytics)
// r.GET("/api/team/members", RequireSubscription("team"), h.GetTeamMembers)

On the frontend, show or hide features based on the user's plan:

Plan Check on Frontend
// After login, the user object includes subscription info
// user.subscription_plan = "pro"
// user.subscription_status = "active"

// Show/hide features based on plan:
// If plan is "free": show upgrade banner
// If plan is "pro": show Pro features, hide Team features
// If plan is "team": show all features

// On the dashboard:
// Free users see: "Upgrade to Pro for advanced analytics"
// Pro users see: the analytics dashboard
// Team users see: analytics + team management
9

Challenge: Add Subscription Checking

Add the SubscriptionPlan and SubscriptionStatus fields to your User model. Create a RequireSubscription middleware. Protect a route (e.g.,/api/analytics) with RequireSubscription("pro", "team"). Test: (1) access the route as a free user (should get 403), (2) manually set a user's plan to"pro" in the database, (3) access the route again (should get 200).

Summary

You've learned everything needed to monetize your Grit SaaS application:

  • Stripe fundamentals — payment gateway, PCI compliance, test vs live mode
  • grit-stripe plugin — checkout sessions, portal sessions, webhook handling
  • Configuration — API keys, webhook secrets, test card numbers
  • Checkout Sessions — Stripe-hosted payment pages, no PCI burden
  • Subscription Plans — Products and Prices in the Stripe Dashboard
  • Webhooks — server-to-server event notifications for payment lifecycle
  • Customer Portal — Stripe-hosted subscription management page
  • Subscription middleware — protect routes based on user's plan
10

Challenge: Final Challenge: Pricing Page + Checkout

Build the complete checkout flow:

  1. Create a pricing page with Free, Pro ($20/mo), and Team ($50/mo) tiers
  2. Each paid plan's button calls POST /api/billing/checkout
  3. User is redirected to Stripe, completes payment with test card
  4. Success URL shows a "Welcome to Pro!" page
11

Challenge: Final Challenge: Webhooks + Activation

Complete the webhook handling:

  1. Handle checkout.session.completed — activate the subscription in your database
  2. Handle invoice.payment_failed — send a notification (log it for now)
  3. Handle customer.subscription.deleted — downgrade to free plan
  4. Test: complete a checkout, verify the user's plan changed in the database
12

Challenge: Final Challenge: Full Billing System

Build the complete billing system:

  1. Pricing page with 3 plans and checkout flow
  2. Webhook handling for activation, failure, and cancellation
  3. Customer portal link on the billing settings page
  4. Subscription status displayed on the user dashboard
  5. RequireSubscription middleware protecting Pro and Team routes
  6. Upgrade banner for free users on restricted pages