Globalne płatności w aplikacji SaaS, czyli integracja ze Stripem

Chcesz globalnie sprzedawać oprogramowanie w modelu abonamentowym? Cyklicznie pobierać płatności i automatycznie wystawiać faktury? Wystarczy jeden system i jeden wolny wieczór…

W tym artykule opisałem integrację ze Stripem w kilku krokach!

Alternatywne rozwiązania

Dlaczego Stripe? Jest to w zasadzie bramka płatności, a nie dedykowane narzędzie do zarządzania subskrypcjami SaaS.

Mógłbym wykorzystać Chargebee, czyli typowy Subscription Billing Software. Prawdopodobnie najlepsze rozwiązanie na rynku, choć cennik nieco przytłacza… Oczywiście mógłbym skorzystać z programu dla startupów (pierwsze $50K przychodu za free) lub wypróbować tańszy odpowiednik z Indii – Pabbly.

Problem? Muszę płacić miesiąc w miesiąc niezależnie od tego, czy generuje przychód. Dodatkowo i tak potrzebuję globalnej bramki płatności, z którą integrują się wyżej wymienione systemy.

Jeśli zagłębisz się w dokumentację Stripe, to okazuję się, że znajdziesz tam większość funkcjonalności dostępnych w dedykowanych rozwiązaniach. Niestety nie wyklikasz wszystkiego – integracja wymaga nieco więcej kodu i pracy programistycznej. W zamian za to masz jeden system do ogarnięcia i płacisz za niego tylko, gdy pobierasz płatności za swoją aplikację – brak kosztów stałych 🙂

Przy dużej liczbie płacących użytkowników zmiana systemu płatności jest bardzo uciążliwa, dlatego wybór odpowiedniego dostawcy na starcie jest kluczowy.

Rejestracja i aktywacja konta

Po rejestracji TUTAJ, otrzymasz krótką ankietę. Odpowiedzi determinują wygląd głównego panelu i dostępność poszczególnych modułów Stripe po zalogowaniu.

Moje odpowiedzi

Pierwszym krokiem do odhaczenia na głównym panelu jest aktywacja konta.

Po kliknięciu „Start” zostaniesz przeniesiony do formularza, gdzie wpisujesz podstawowe dane na temat Twojego produktu i firmy.

W kolejnych krokach aktywacja wymaga potwierdzenia tożsamości i adresu. Wystarczy przesłać skan dowodu osobistego i np. jakiś rachunek lub potwierdzenie dowolnego przelewu bankowego, na którym widnieje adres.

Ilość i rodzaj wymaganych dokumentów może różnić się w zależności od rodzaju prowadzonej działalności. TUTAJ znajdziesz więcej info na temat aktywacji konta, a TUTAJ o akceptowanych dokumentach.

Procedura przebiega bardzo sprawnie, a akceptacja trwa maksymalnie 24 godziny. Nie trzeba nic drukować i podpisywać w przeciwieństwie do polskiego Tpay.

Testowanie

Po aktywacji możemy zabrać się za integrację. Na tym etapie nie chcemy przyjmować prawdziwych płatności, tylko dodawać kolejne elementy i testować cały proces za darmo.

Po lewej stronie na dashboardzie znajdziesz przełącznik „Viewing test data”. Upewnij się, że jest włączony przez dalszą część konfiguracji.

Do testów możesz wykorzystać serię testowych kart, które znajdują się TUTAJ. Jeśli wszystko będzie działać zgodnie z założeniami, to całą konfigurację przeniesiesz na produkcję – z wyłączonym switchem.

Definicja produktu i planów

Przejdź do zakładki Billing > Products i utwórz produkt reprezentujący Twoją aplikację. W moim przypadku Customer24.

Następnie do produktu dodaj kolejne pricing plany z odpowiednimi cenami.

Ile planów? To zależy od Twojego modelu biznesowego. U mnie są 3 plany, każdy można opłacić w EUR lub PLN, rocznie lub miesięcznie, czyli łącznie 12: BasicMonthlyEN, BasicMonthlyPL, BasicYearlyEN, BasicYearlyPL, PlusMonthlyEN, PlusMonthlyPL i tak dalej.

Integracja po stronie serwera (backend)

Lecimy zgodnie z tym fragmentem dokumentacji. Plany już masz, wiec przejdź do punktu 2 i utwórz tzw. checkout session po stronie serwera.

W dokumentacji zostały umieszczone przykładowe fragmenty kodu w różnych językach programowania. Ja piszę API w .NET Corze i pokażę Ci, jak to wygląda u mnie 😉

Podpinam paczkę NuGet Stripe.net, przez którą łatwo operuję na Stripe API bez sklejania zapytań na własną rękę. Oczywiście uwierzytelniam się kluczem API pobranym z głównego panelu (sekcja Developers > API keys).

StripeConfiguration.ApiKey = configuration["Stripe:ApiKey"];

Sposób organizacji kodu (architektura) nie ma znaczenia, więc dla uproszczenia będę wklejał kluczowe fragmenty metod. Możesz opakować je w dowolne abstrakcje – serwisy z logiką biznesową lub komendy/kwerendy w CQRS.

Wystawiam łącznie 5 RESTowych endpointów.

1. Subskrypcja – POST billing/subscribe

Tutaj generuję checkout session pod aktywację subskrypcji (wykupienie planu) przez nowego użytkownika lub starego, który nie ma aktualnie żadnej aktywnej subskrypcji.

Każdy plan ma swój unikalny identyfikator na panelu Stripe. Kopiuję wszystkie i wrzucam do pliku konfiguracyjnego projektu, aby łatwo konwertować nazwę (enum) planu na jego id.

// jeśli nowy klient, to tworzę go w Stripe (widoczny na dashboardzie)
if (user.Company.BillingCustomerId == null)
{
    var customer = await CreateCustomer(user);
    user.Company.BillingCustomerId = customer.Id;
}
// jeśli był wcześniej subskrybentem, to aktualizuję jego dane w Stripe
else
{
    await UpdateCustomer(user);
}

// konwertuję nazwę planu na jego id w Stripe
var planId = _configuration[$"Stripe:{plan}{_currentLanguage}"];

var options = new SessionCreateOptions
{
    PaymentMethodTypes = new List<string> { "card" },
    SubscriptionData = new SessionSubscriptionDataOptions
    {
        Items = new List<SessionSubscriptionDataItemOptions>
        {
            new SessionSubscriptionDataItemOptions
            {
                Plan = planId,
                Quantity = 1
            }
        }
    },
    Customer = user.Company.BillingCustomerId,
    ClientReferenceId = userId,

    // adresy do frontendu – przekierowania po zakończonej sesji
    SuccessUrl = _clientUrl, // główna strona
    CancelUrl = _clientUrl + "/#/billing" // powrót do zakładki z wyborem planu
};

var service = new SessionService();
var session = await service.CreateAsync(options);

// zwracam identyfikator wygenerowanej sesji
result.Data = session.Id;

Jak wygląda tworzenie i aktualizacja klienta? Po prostu przesyłam do Stripe dane z bazy, które użytkownik podał w trakcie rejestracji lub aktualizacji profilu firmy.

// utworzenie klienta z danymi do faktury
private async Task<Customer> CreateCustomer(User admin)
{
    var company = admin.Company;
    var options = new CustomerCreateOptions
    {
        Name = company.Name ?? admin.FullName,
        Address = new AddressOptions
        {
            Line1 = company.AddressLine1,
            Line2 = company.AddressLine2,
            PostalCode = company.AddressPostalCode,
            City = company.AddressCity,
            State = company.AddressState,
            Country = company.Country?.Name
        },
        Phone = admin.PhoneNumber,
        Email = admin.Email
    };

    var service = new CustomerService();
    return await service.CreateAsync(options);
}

// analogicznie aktualizacja
private async Task UpdateCustomer(User admin)
{
    ...
    var service = new CustomerService();
    await service.UpdateAsync(company.BillingCustomerId, options);
}

Co więcej, jeśli użytkownik jest aktywnym subskrybentem, to aktualizuje jego dane w Stripe za każdym razem, gdy modyfikuje profil w aplikacji.

2. Zmiana planu – PUT billing/change

Endpoint umożliwiający zmianę pricing planu, czyli użytkownik wchodzi na wyższy plan (up-selling) lub niższy (down-selling). Stripe automatycznie kompensuje płatności, więc kolejne faktury są powiększane lub pomniejszane o różnicę cen. Przyjemnie prawda? 🙂

Zmiana sprowadza się do pobrania i aktualizacji subskrypcji.

var service = new SubscriptionService();
var subscription = service.Get(user.Company.BillingSubscriptionId);

var items = new List<SubscriptionItemUpdateOption>
{
    new SubscriptionItemUpdateOption
    {
        Id = subscription.Items.Data[0].Id,

        // ustawiam nowy plan
        Plan = _configuration[$"Stripe:{plan}{_currentLanguage}"];
    }
};

var options = new SubscriptionUpdateOptions
{
    CancelAtPeriodEnd = false,
    Items = items,
};

await service.UpdateAsync(subscription.Id, options);

3. Aktualizacja karty – PUT billing/update

W tym miejscu generuję sesję, która umożliwi użytkownikowi aktualizację danych karty płatniczej na frontendzie.

var options = new SessionCreateOptions
{
    PaymentMethodTypes = new List<string> { "card" },
    SetupIntentData = new SessionSetupIntentDataOptions
    {
        Metadata = new Dictionary<string, string>
        {
            {"customer_id", user.Company.BillingCustomerId},
            {"subscription_id", user.Company.BillingSubscriptionId}
        }
    },
    CustomerEmail = user.Email,
    ClientReferenceId = userId,
    Mode = "setup",
    SuccessUrl = _clientUrl,
    CancelUrl = _clientUrl + "/#/billing"
};

var service = new SessionService();
var session = await service.CreateAsync(options);

result.Data = session.Id;

4. Anulowanie subskrypcji – DELETE billing/unsubscribe

Jeśli użytkownik nie chce dłużej płacić abonamentu, to w prosty i przejrzysty sposób pozwalam mu odejść – anulować subskrypcję.

var service = new SubscriptionService();
var cancelOptions = new SubscriptionCancelOptions
{
    InvoiceNow = false,
    Prorate = false,
};

await service.CancelAsync(user.Company.BillingSubscriptionId, cancelOptions);

Wbrew różnym mitom, utrudnianie rezygnacji nie poprawia retencji, natomiast ma bardzo zły wpływ na reputację marki. Niejasna komunikacja modelu rozliczeniowego oraz utrudnianie rezygnacji z subskrypcji mogą prowadzić do chargebacków, czyli żądań zwrotu środków za pośrednictwem banku i organizacji kartowej.

Michał Jędraszak

5. Webhooki – reagowanie na zdarzenia Stripe

Stripe emituje zdarzenia, aby poinformować naszą aplikację (serwer) np. o ukończeniu sesji wprowadzania danych karty płatniczej. Wysyła również informację o zmianach subskrypcji lub jej usunięciu (anulowaniu). Dzięki temu zawsze w bazie po stronie serwera masz informację o aktywnej subskrypcji użytkownika.

Dodatkowo niektóre operacje np. aktualizacja karty płatniczej wymagają „manualnych” zmian przez Stripe API (ustawienia domyślnej metody płatności) po zakończeniu checkout session.

Na starcie musisz zbudować tzw. webhook endpoint zgodnie z przykładami w dokumentacji.

Poniżej zamieściłem kluczowe fragmenty kodu odpowiedzialnego za obsługę zdarzeń – po uproszczeniu (bez transakcji i podziału).

var stripeEvent = EventUtility.ConstructEvent(body, signature, _secret);

// https://stripe.com/docs/api/events/types
switch (stripeEvent.Type)
{
case Events.CheckoutSessionCompleted:
    var session = stripeEvent.Data.Object as Session;

    var user = await _context.Users.FirstOrDefaultAsync(
        x => x.Id == session.ClientReferenceId);

    if (user == null)
    {
        _logger.LogCritical("Can't find ClientReferenceId", session.ClientReferenceId);
        result.Error = ErrorType.NotFound;
        return result;
    }

    // aktualizacja danych karty wymaga zmiany domyślnej metody płatności
    if (session.Mode == "setup")
    {
        var setupService = new SetupIntentService();
        var setupIntent = await setupService.GetAsync(session.SetupIntentId);

        var paymentService = new PaymentMethodService();
        await paymentService.AttachAsync(
            setupIntent.PaymentMethodId,
            new PaymentMethodAttachOptions
            {
                Customer = user.Company.BillingCustomerId
            });

        var subscriptionService = new SubscriptionService();
        await subscriptionService.UpdateAsync(
            user.Company.BillingSubscriptionId,
            new SubscriptionUpdateOptions
            {
                DefaultPaymentMethod = setupIntent.PaymentMethodId
            });

        return result;
    }

    var plan = session.DisplayItems[0].Plan;

    user.Company.Plan = Enums.GetEnum<PricingPlan>(plan.Nickname);
    user.Company.PlanExpiredOn = null;
    user.Company.BillingSubscriptionId = session.SubscriptionId;
    user.Company.BillingCustomerId = session.CustomerId;

    _context.Companies.Update(user.Company);

    break;
case Events.CustomerSubscriptionUpdated:
    var subscription = stripeEvent.Data.Object as Subscription;

    var company = await _context.Companies.FirstOrDefaultAsync(
        x => x.BillingSubscriptionId == subscription.Id);

    if (company == null)
    {
        _logger.LogCritical("Can't find SubscriptionId", subscription);
        result.Error = ErrorType.NotFound;
        return result;
    }

    company.Plan = Enums.GetEnum<PricingPlan>(subscription.Plan.Nickname);
    company.PlanExpiredOn = null;

    break;
case Events.CustomerSubscriptionDeleted:
    subscription = stripeEvent.Data.Object as Subscription;

    company = await _context.Companies.FirstOrDefaultAsync(
        x => x.BillingSubscriptionId == subscription.Id);

    if (company == null)
    {
        _logger.LogCritical("Can't find SubscriptionId", subscription);
        result.Error = ErrorType.NotFound;
        return result;
    }

    company.BillingSubscriptionId = null;
    company.PlanExpiredOn = subscription.CurrentPeriodEnd.Value.AddDays(-1);

    break;
}

Następnie przejdź do zakładki Developers > Webhooks i dodaj swoje endpointy. Mogą być oddzielne lub jeden do obsługi wszystkich zdarzeń jak na zrzucie przedstawionym poniżej.

Brawo! Właśnie zintegrowałeś Stripe z backendem swojej aplikacji 🙂

Integracja interfejsu użytkownika (frontend)

Jak to wygląda na froncie? Możesz wykorzystać dowolny framework JS – w moim przypadku jest to Angular.

Przygotuj kawałek UI odpowiedzialny za wybór planu i sposobu rozliczenia (roczny vs miesięczny). Pod każdym planem wypisz listę funkcjonalności i ograniczeń.

Przykładowy widok – aktywna subskrypcja Professional

Kliknięcia przycisków powodują wysłanie zapytań z określonymi parametrami do opisanego wcześniej API. Oczywiście poprzedzone popupem z pytaniem, czy na pewno 😉

Metody POST billing/subscribe oraz PUT billing/update zwracają identyfikator checkout session, który wykorzystuję do wyświetlenia formularza Stripe Checkout.

Jak to zrobić? Dołącz skrypt Stripe.js do index.html i wykonuj następujący kod po kliknięciu przycisku na interfejsie.

onSubscribe(plan: PricingPlan) {
    // wysłanie zapytania HTTP POST billing/subscribe
    this.billingService.subscribe(plan)
        .subscribe(
            id => {
                const stripe = Stripe(environment.stripeApiKey);
                stripe.redirectToCheckout({
                    sessionId: id
                }).then(result => {
                    if (result.error) {
                        this.toastService.error();
                    }
                });
            },
            () => this.toastService.error()
        );
}

Analogicznie aktualizacja karty płatniczej. Po przekierowaniu użytkownik wyląduje na ekranie płatności.

Prawidłowo wprowadzone dane karty zostaną zapisane w Stripe. Nastąpi emisja zdarzenia checkout.session.completed oraz przekierowanie do SuccessUrl, który podałeś generując sesję po stronie serwera. That’s it!

W razie problemów z obsługą zdarzeń koniecznie przejrzyj logi w sekcji Developers > Logs.

Co z fakturami?

Stripe automatycznie generuje faktury dla Twoich klientów. Wystarczy uzupełnić Public Business Information w zakładce Settings > Account information, włączyć wysyłkę faktur i rachunków na email w Settings > Subscriptions and emails oraz Settings > Email receipts.

Ostatnim krokiem jest dostosowanie wyglądu rachunków, faktur i strony checkout w sekcji Settings > Branding.

Wszystkie informacje o Twoich klientach, wystawionych fakturach, subskrypcjach i metodach płatności znajdują się na dashboardzie Stripe. Zawsze możesz edytować je manualnie z poziomu panelu np. gdy klient poprosi o ponowne wystawienie faktury na inne dane.

Jak to księgować i rozliczać z urzędem skarbowym? Wchodzisz do zakładki Billing > Invoices, eksportujesz wszystko i wysyłasz do swojej księgowej. Ona (lub on) już wie, co z tym zrobić 🙂

Podsumowanie

W tym artykule pokazałem, jak od zera zintegrować usługę Stripe z globalną aplikacją typu SaaS. Automatyzacja procesu pobierania płatności i rozliczeń w modelu abonamentowym nie musi być skomplikowana.

Stripe wszedł do Polski w drugiej połowie 2019 roku i według mnie jest najlepszym rozwiązaniem dla mniejszych startupów. Jest to moja subiektywna opinia – wpis nie jest sponsorowany przez Stripe 😀

A Ty z czego aktualnie korzystasz? Który system płatności według Ciebie jest najlepszy i dlaczego nie Stripe? Daj znać w komentarzu!

Podobał Ci się ten artykuł?

Jeśli tak, to udostępnij go w Social Media i zostaw maila o TUTAJ, aby otrzymywać powiadomienia o nowych artykułach i materiałach!