· Stripe· SaaS· Checkout· abonnement· subscription· trial· Python· FastAPI· betalinger
Stripe Checkout til SaaS — abonnement, trial og hardware-køb
Stripe Checkout og Billing til SaaS: subscription oprettelse, trial-perioder, hardware-køb som one-time charge, customer portal og proration ved plan-skift.
Af M-Bus Gateway
M-Bus Gateway platformen kombinerer Stripe Subscriptions (månedlig SaaS) med one-time Checkout til hardware. Her er implementeringen.
To produkttyper i Stripe
M-Bus Gateway Stripe-produkter:
1. Software-abonnement (recurring):
Pris: 49 kr./enhed/md (variabel baseret på aktive lejemål)
Type: Stripe Subscription med metered billing
Trial: 30 dage gratis (kreditkort kræves ved signup)
2. Hardware (one-time):
Installationspakke S: 3.500 kr. (Gateway + 6 HCA)
Installationspakke L: 7.200 kr. (Gateway + 15 HCA)
Ekstra HCA: 320 kr./stk.
Type: Stripe Checkout one-time payment
Workflow:
Udlejer opretter konto → 30-dages trial starter
→ Bestiller hardware (Checkout)
→ Hardware leveres, installeres
→ Trial udløber → Abonnement starter (per aktivt lejemål)
Subscription-oprettelse ved registrering
# server/src/stripe/service.py
import stripe
from server.src.core.config import settings
stripe.api_key = settings.STRIPE_SECRET_KEY
def create_customer_and_subscription(
email: str,
tenant_id: str,
trial_days: int = 30,
) -> dict:
"""
Opret Stripe Customer + Subscription med trial.
Kræver IKKE betalingskort ved oprettelse (trial_period_days).
"""
# Opret Stripe Customer:
customer = stripe.Customer.create(
email=email,
metadata={"tenant_id": str(tenant_id)},
)
# Opret Subscription med trial:
subscription = stripe.Subscription.create(
customer=customer.id,
items=[
{
"price": settings.STRIPE_PRICE_ID_PER_UNIT, # pr. aktiv enhed
"quantity": 0, # Starter med 0 — justeres månedligt via Celery
}
],
trial_period_days=trial_days,
payment_settings={"save_default_payment_method": "on_subscription"},
expand=["latest_invoice.payment_intent"],
)
return {
"stripe_customer_id": customer.id,
"stripe_subscription_id": subscription.id,
"trial_end": subscription.trial_end,
"status": subscription.status,
}
Stripe Checkout til hardware
# server/src/stripe/checkout.py
HARDWARE_PRODUCTS = {
"pakke_s": settings.STRIPE_PRICE_ID_PAKKE_S, # 3.500 kr. one-time
"pakke_l": settings.STRIPE_PRICE_ID_PAKKE_L, # 7.200 kr. one-time
"hca_enkelt": settings.STRIPE_PRICE_ID_HCA, # 320 kr./stk.
}
def create_hardware_checkout_session(
tenant_id: str,
stripe_customer_id: str,
items: list[dict], # [{"product": "pakke_s", "quantity": 1}]
success_url: str,
cancel_url: str,
) -> str:
"""Returner Stripe Checkout URL til hardware-køb."""
line_items = [
{
"price": HARDWARE_PRODUCTS[item["product"]],
"quantity": item["quantity"],
}
for item in items
]
session = stripe.checkout.Session.create(
customer=stripe_customer_id,
mode="payment", # One-time payment (ikke subscription)
line_items=line_items,
success_url=success_url,
cancel_url=cancel_url,
metadata={"tenant_id": str(tenant_id)},
# Automatisk moms (Stripe Tax):
automatic_tax={"enabled": True},
# Faktura som PDF:
invoice_creation={"enabled": True},
)
return session.url
Customer Portal: Selvbetjening
# server/src/stripe/portal.py
def create_customer_portal_session(
stripe_customer_id: str,
return_url: str,
) -> str:
"""
Stripe Customer Portal: Lejer kan selv:
- Opdatere betalingsmetode
- Se fakturahistorik
- Annullere abonnement
"""
session = stripe.billing_portal.Session.create(
customer=stripe_customer_id,
return_url=return_url,
)
return session.url
@router.get("/billing/portal")
async def get_billing_portal(
user: User = Depends(require_role("landlord")),
session: AsyncSession = Depends(get_session),
):
"""Redirect til Stripe Customer Portal."""
tenant = await session.get(Tenant, user.tenant_id)
portal_url = create_customer_portal_session(
stripe_customer_id=tenant.stripe_customer_id,
return_url=f"{settings.BASE_URL}/portal/profile",
)
return {"portal_url": portal_url}
Webhook-handlers
# server/src/stripe/router.py
from fastapi import APIRouter, Request, HTTPException
import stripe
router = APIRouter(prefix="/webhooks/stripe", tags=["stripe"])
@router.post("")
async def stripe_webhook(
request: Request,
session: AsyncSession = Depends(get_session),
):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except stripe.error.SignatureVerificationError:
raise HTTPException(400, "Invalid signature")
handlers = {
"customer.subscription.created": _handle_subscription_created,
"customer.subscription.updated": _handle_subscription_updated,
"customer.subscription.deleted": _handle_subscription_deleted,
"invoice.payment_failed": _handle_invoice_payment_failed,
"invoice.paid": _handle_invoice_paid,
"checkout.session.completed": _handle_checkout_completed,
}
handler = handlers.get(event["type"])
if handler:
await handler(event["data"]["object"], session)
return {"received": True}
async def _handle_checkout_completed(checkout_session: dict, session: AsyncSession):
"""Hardware-ordre gennemført → opret ordre i system."""
tenant_id = checkout_session["metadata"]["tenant_id"]
# Opret ordre, send bekræftelsesmail, registrér hardware-enheder
Proration ved plan-skift
# Automatisk proration ved skift af antal enheder:
# (Stripe håndterer det automatisk ved quantity-opdatering)
async def update_subscription_quantity(
stripe_subscription_id: str,
stripe_subscription_item_id: str,
new_quantity: int,
):
"""
Opdatér antal fakturerede enheder.
Stripe beregner automatisk proration for indeværende periode.
"""
stripe.SubscriptionItem.modify(
stripe_subscription_item_id,
quantity=new_quantity,
proration_behavior="create_prorations", # Beregn og opret proration-linje
)
Konklusion
Stripe Checkout kombinerer hardware-salg (one-time) og SaaS-abonnement (recurring) i ét system. Trial med 30 dage og ingen kreditkort ved oprettelse maximerer konvertering. Customer Portal giver selvbetjening uden support-tickets. Webhook-handlers med signaturverifikation er kritisk for sikkerhed.