Back

Stripe i Next.js: Komplett guide til betalingsintegrasjon

Implementer Stripe-betalinger i Next.js på riktig måte. Lær checkout, webhooks, abonnementer og norske betalingsløsninger.

Ronny Bruknapp
Ronny Bruknapp
December 24, 2025
Updated Dec 24, 2025
Share:

Stripe i Next.js: Komplett guide til betalingsintegrasjon

Betalinger er det som gjør en hobby-app til en forretning. Men betalingsintegrasjon kan virke skremmende — sikkerhet, PCI-compliance, webhooks, og edge cases overalt.

Stripe gjør dette enkelt. Og med riktig oppsett i Next.js, kan du ha fungerende betalinger på under en time.

Denne guiden tar deg gjennom alt: fra enkel checkout til abonnementer, webhooks og norske betalingsløsninger.

Hvorfor Stripe for norske utviklere?

Stripe er den foretrukne betalingsløsningen for moderne webapplikasjoner:

EgenskapStripe
Transaksjonsgebyr (Norge/EU)2,4% + 2 NOK
Internasjonale kort3,25% + 2 NOK
Ingen faste kostnader
AbonnementshåndteringInnebygd
UtviklerverktøyFremragende
DokumentasjonBeste i klassen

Stripe holder på pengene i 7 dager før utbetaling til din konto. For raskere utbetaling, vurder å søke om "Instant Payouts".

Grunnleggende oppsett

Installer avhengigheter

npm install stripe @stripe/stripe-js

Konfigurer miljøvariabler

Legg til i .env.local:

STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

NEXT_PUBLIC_-prefikset eksponerer variabelen til nettleseren. Bruk ALDRI dette for hemmelige nøkler som STRIPE_SECRET_KEY!

Server-side Stripe-instans

// lib/stripe.ts
import Stripe from "stripe";

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error("STRIPE_SECRET_KEY environment variable is required");
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2024-11-20.acacia",
  typescript: true,
});

Stripe Checkout

Den enkleste måten å ta imot betalinger er med Stripe Checkout — en ferdig betalingsside som Stripe hoster.

Opprett checkout-sesjon

// app/api/checkout/route.ts
import { auth } from "@/lib/auth";
import { stripe } from "@/lib/stripe";

export async function POST(request: Request) {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { priceId } = await request.json();

  const checkoutSession = await stripe.checkout.sessions.create({
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [
      {
        price: priceId,
        quantity: 1,
      },
    ],
    customer_email: session.user.email,
    metadata: {
      userId: session.user.id,
    },
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
  });

  return Response.json({ url: checkoutSession.url });
}

Client-side redirect

// components/checkout-button.tsx
"use client";

import { toast } from "sonner";

export function CheckoutButton({ priceId }: { priceId: string }) {
  const handleCheckout = async () => {
    try {
      const response = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ priceId }),
      });

      const { url } = await response.json();
      window.location.href = url;
    } catch (error) {
      toast.error("Noe gikk galt. Prøv igjen.");
    }
  };

  return (
    <button onClick={handleCheckout}>
      Oppgrader til Premium
    </button>
  );
}

Webhooks — Kritisk viktig

Webhooks er hvordan Stripe forteller deg at noe har skjedd — en betaling er fullført, et abonnement er kansellert, etc.

Opprett webhook-handler

// app/api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error("Webhook signature verification failed");
    return Response.json({ error: "Invalid signature" }, { status: 400 });
  }

  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object;
      const userId = session.metadata?.userId;

      if (userId) {
        await db.insert(subscriptions).values({
          id: crypto.randomUUID(),
          userId,
          stripeCustomerId: session.customer as string,
          stripeSubscriptionId: session.subscription as string,
          status: "active",
        });
      }
      break;
    }

    case "customer.subscription.updated":
    case "customer.subscription.deleted": {
      const subscription = event.data.object;
      await db
        .update(subscriptions)
        .set({
          status: subscription.status,
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
        })
        .where(eq(subscriptions.stripeSubscriptionId, subscription.id));
      break;
    }
  }

  return Response.json({ received: true });
}

Lokal webhook-testing

I development, bruk Stripe CLI:

# Installer Stripe CLI
brew install stripe/stripe-cli/stripe

# Logg inn
stripe login

# Forward webhooks til localhost
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Stripe CLI gir deg en midlertidig webhook-secret som starter med whsec_. Bruk denne i .env.local under development.

Abonnementshåndtering

Sjekk abonnementsstatus

// lib/subscription.ts
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

export async function getSubscription(userId: string) {
  const subscription = await db.query.subscriptions.findFirst({
    where: eq(subscriptions.userId, userId),
  });

  if (!subscription) return null;

  return {
    isActive: subscription.status === "active",
    isPastDue: subscription.status === "past_due",
    currentPeriodEnd: subscription.currentPeriodEnd,
  };
}

Kundeportal for selvbetjening

La brukere administrere sitt abonnement selv:

// app/api/billing/portal/route.ts
import { auth } from "@/lib/auth";
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

export async function POST(request: Request) {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  const subscription = await db.query.subscriptions.findFirst({
    where: eq(subscriptions.userId, session.user.id),
  });

  if (!subscription?.stripeCustomerId) {
    return Response.json({ error: "No subscription found" }, { status: 404 });
  }

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: subscription.stripeCustomerId,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  });

  return Response.json({ url: portalSession.url });
}

Norske betalingsmetoder

Stripe støtter flere betalingsmetoder populære i Norge:

const checkoutSession = await stripe.checkout.sessions.create({
  mode: "payment",
  payment_method_types: [
    "card",        // Visa, Mastercard
    "klarna",      // Klarna (populært i Norge)
    "mobilepay",   // MobilePay (Danmark/Norge)
  ],
  // ... resten av konfigurasjonen
});

Vipps støttes ikke direkte av Stripe. For Vipps-integrasjon, bruk Vipps' egen løsning eller en gateway som støtter begge.

PCI-compliance

Stripe håndterer all PCI-compliance for deg når du bruker deres løsninger:

  • Stripe Checkout — Fullstendig håndtert av Stripe
  • Stripe Elements — Kort-input rendres i sikre iframes
  • Payment Intents — Kortdata berører aldri din server

Du trenger aldri å se eller lagre kortdetaljer.

Integrasjon med resten av stacken

Stripe-integrasjonen fungerer sømløst med LLM-First Boilerplate-stacken:

  1. Better Auth — Autentisering for å koble betalinger til brukere
  2. Drizzle ORM — Lagre abonnementer i PostgreSQL
  3. Sonner toast — Brukernotifikasjoner for betalingsstatus

Som vi diskuterer i AI-først utvikling, handler god arkitektur om konsistente mønstre. Stripe-integrasjonen følger samme mønster som resten av kodebasen.

Feilhåndtering

Robust feilhåndtering er kritisk for betalinger:

import { toast } from "sonner";

async function handlePayment() {
  try {
    const response = await fetch("/api/checkout", {
      method: "POST",
      body: JSON.stringify({ priceId }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || "Betalingen feilet");
    }

    const { url } = await response.json();
    window.location.href = url;
  } catch (error) {
    if (error instanceof Error) {
      toast.error(error.message);
    } else {
      toast.error("Noe gikk galt. Prøv igjen senere.");
    }
  }
}

Testing

Stripe tilbyr test-kort for ulike scenarioer:

KortResultat
4242 4242 4242 4242Vellykket betaling
4000 0000 0000 0002Avvist kort
4000 0000 0000 9995Insufficient funds
4000 0000 0000 32203D Secure påkrevd

Bruk hvilken som helst fremtidig utløpsdato og CVC.

Konklusjon

Stripe-integrasjon i Next.js trenger ikke være komplisert. Med riktig oppsett får du:

  • Sikker betalingshåndtering uten PCI-bekymringer
  • Abonnementer med automatisk fornyelse
  • Webhooks for real-time oppdateringer
  • Kundeportal for selvbetjening

LLM-First Boilerplate gir deg Stripe ferdig konfigurert, slik at du kan fokusere på å bygge produktet ditt.

God jul og godt salg! 🎄

Les mer

Kilder

Klar til å tjene penger på appen din? Start med LLM-First Boilerplate og ha Stripe-betalinger oppe på timer, ikke dager.