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.
The payment flow with Stripe:
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 subscriptionChallenge: 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.
# Install the plugin
go get github.com/MUKE-coder/grit-plugins/grit-stripeWhat 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
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).
# 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_secretInitialize the plugin in your API:
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"),
})
}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.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.
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:
// 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 URLChallenge: 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:
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_monthlyCreate these in the Stripe Dashboard:
- 1.Go to Products in the Stripe Dashboard
- 2.Click "Add Product"
- 3.Name: "Pro Plan", Pricing: Recurring, Amount: $20.00, Billing period: Monthly
- 4.Save — Stripe generates a Price ID (starts with
price_) - 5.Repeat for Team Plan ($50/month)
.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.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.
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):
// Webhook endpoint — no auth middleware!
// Stripe authenticates via webhook signature, not JWT
r.POST("/api/webhooks/stripe", h.HandleStripeWebhook)STRIPE_WEBHOOK_SECRET, and the plugin verifies the signature automatically.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.
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/portalThe 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
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.
// 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 = urlDesign 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
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:
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
}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:
// 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 managementChallenge: 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
Challenge: Final Challenge: Pricing Page + Checkout
Build the complete checkout flow:
- Create a pricing page with Free, Pro ($20/mo), and Team ($50/mo) tiers
- Each paid plan's button calls
POST /api/billing/checkout - User is redirected to Stripe, completes payment with test card
- Success URL shows a "Welcome to Pro!" page
Challenge: Final Challenge: Webhooks + Activation
Complete the webhook handling:
- Handle
checkout.session.completed— activate the subscription in your database - Handle
invoice.payment_failed— send a notification (log it for now) - Handle
customer.subscription.deleted— downgrade to free plan - Test: complete a checkout, verify the user's plan changed in the database
Challenge: Final Challenge: Full Billing System
Build the complete billing system:
- Pricing page with 3 plans and checkout flow
- Webhook handling for activation, failure, and cancellation
- Customer portal link on the billing settings page
- Subscription status displayed on the user dashboard
RequireSubscriptionmiddleware protecting Pro and Team routes- Upgrade banner for free users on restricted pages
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.