Siirry sisältöön

Stripe-integraatio

Kirjapro käyttää Stripea tilauslaskutukseen. Tämä dokumentaatio kuvaa integraation toiminnan kehittäjille.

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Kirjapro │────▶│ Supabase Edge │────▶│ Stripe API │
│ Frontend │ │ Functions │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Tietokanta │◀────│ Webhooks │
│ │ │ (tilaukset) │
└──────────────────┘ └─────────────────┘
TasoKuukausiVuosiOminaisuudet
Ilmainen0 €0 €Perustoiminnot
Yrittäjä19 €190 €Rajaton laskutus, pankkiyhteys
Yritys49 €490 €Kaikki ominaisuudet, 5 käyttäjää
  1. Käyttäjä valitsee hinnoittelusivulla haluamansa paketin
  2. Frontend kutsuu create-checkout-session endpointia
  3. Edge function luo Stripe Checkout -session
  4. Käyttäjä ohjataan Stripen maksusivulle
  5. Maksun jälkeen webhook päivittää tilauksen
const response = await fetch(
`${SUPABASE_URL}/functions/v1/create-checkout-session`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'apikey': SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
priceId: 'price_xxx',
companyId: 'uuid-here'
}),
}
);
const { checkoutUrl } = await response.json();
window.location.href = checkoutUrl;
{
"success": true,
"checkoutUrl": "https://checkout.stripe.com/c/pay/...",
"sessionId": "cs_xxx"
}

Tilauksen hallinta tapahtuu Stripen asiakasportaalissa.

const response = await fetch(
`${SUPABASE_URL}/functions/v1/create-portal-session`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'apikey': SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
companyId: 'uuid-here',
returnUrl: 'https://app.kirjapro.fi/settings'
}),
}
);
const { portalUrl } = await response.json();
window.location.href = portalUrl;

Asiakasportaalissa voi:

  • Päivittää maksutapaa
  • Vaihtaa tilaustasoa
  • Nähdä laskuhistorian
  • Peruuttaa tilauksen

Stripe lähettää webhook-kutsuja tilauksen tilan muuttuessa.

TapahtumaKuvaus
checkout.session.completedUusi tilaus aloitettu
customer.subscription.updatedTilaus päivitetty (taso, jakso)
customer.subscription.deletedTilaus peruttu
invoice.paidLasku maksettu
invoice.payment_failedMaksu epäonnistui
{
"type": "customer.subscription.updated",
"data": {
"object": {
"id": "sub_xxx",
"customer": "cus_xxx",
"status": "active",
"items": {
"data": [{
"price": {
"id": "price_xxx",
"unit_amount": 1900,
"recurring": {
"interval": "month"
}
}
}]
},
"current_period_start": 1704067200,
"current_period_end": 1706745600
}
}
}
import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY);
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
rawBody,
sig,
STRIPE_WEBHOOK_SECRET
);
// Käsittele tapahtuma
switch (event.type) {
case 'checkout.session.completed':
// Uusi tilaus
break;
case 'customer.subscription.updated':
// Tilaus päivitetty
break;
}
StatusMerkitys
trialingKokeilujakso käynnissä
activeAktiivinen tilaus
past_dueMaksu myöhässä
canceledPeruttu
// Haetaan yrityksen tilaus
const { data: subscription } = await supabase
.from('subscriptions')
.select('tier, status, current_period_end')
.eq('company_id', companyId)
.single();
if (subscription?.status === 'active') {
// Tilaus voimassa
}
if (subscription?.status === 'past_due') {
// Näytä maksukehotus
}
EndpointRaja
create-checkout-session5 / min
create-portal-session5 / min
stripe-webhookEi rajaa
KoodiViestiRatkaisu
STRIPE_CUSTOMER_NOT_FOUNDEi Stripe-asiakastaKäyttäjän tulee ensin tehdä tilaus
INVALID_PRICE_IDVirheellinen hintaTarkista price ID
SUBSCRIPTION_NOT_FOUNDTilausta ei löydyYritys ei ole tilaaja
PORTAL_SESSION_FAILEDPortaali-istunto epäonnistuiKokeile uudelleen
{
"success": false,
"error": {
"code": "STRIPE_CUSTOMER_NOT_FOUND",
"message": "Yritystä ei löydy Stripe-asiakkaana"
}
}

Käytä Stripen testikortteja kehitysympäristössä:

KorttiTulos
4242 4242 4242 4242Onnistuu
4000 0000 0000 0002Hylätään
4000 0025 0000 3155Vaatii 3D Secure
Terminal window
# Kuuntele webhookeja lokaalisti
stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook
# Lähetä testitapahtuma
stripe trigger checkout.session.completed
import { useState } from 'react';
import { supabase } from '@/lib/supabase';
export function useStripeCheckout() {
const [isLoading, setIsLoading] = useState(false);
const checkout = async (priceId: string, companyId: string) => {
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke(
'create-checkout-session',
{ body: { priceId, companyId } }
);
if (error) throw error;
window.location.href = data.checkoutUrl;
} catch (err) {
console.error('Checkout failed:', err);
throw err;
} finally {
setIsLoading(false);
}
};
const manageSubscription = async (companyId: string) => {
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke(
'create-portal-session',
{ body: { companyId, returnUrl: window.location.href } }
);
if (error) throw error;
window.location.href = data.portalUrl;
} finally {
setIsLoading(false);
}
};
return { checkout, manageSubscription, isLoading };
}
function PricingCard({ priceId, companyId }) {
const { checkout, isLoading } = useStripeCheckout();
return (
<button
onClick={() => checkout(priceId, companyId)}
disabled={isLoading}
>
{isLoading ? 'Ladataan...' : 'Tilaa'}
</button>
);
}
  • Kaikki API-kutsut vaativat autentikoinnin
  • Webhook-allekirjoitukset varmistetaan
  • Return URL:t validoidaan (estää open redirect)
  • Hinnat validoidaan sallittujen joukosta