Billing SaaS cu Stripe: abonamente, webhooks, facturare

Cum implementezi billing recurent într-un SaaS cu Stripe: abonamente, webhooks ca sursă de adevăr, proration, invoicing și recuperarea plăților eșuate.

Andrei Badulescu
Andrei Badulescu28 June 2026 · Actualizat 17 July 2026 · 10 min citit
Ilustrație editorială pe fundal charcoal cu accente portocalii: fluxul de billing recurent al unui SaaS pe Stripe.

Billing-ul e locul unde un SaaS câștigă sau pierde bani — la propriu. Poți avea cel mai curat produs, dar dacă plata recurentă se rupe la al treilea ciclu, ai pierdut clientul fără să afli de ce. Acest ghid arată cum implementezi billing recurent într-un SaaS cu Stripe: de la prima plată din Checkout, prin ciclul de viață al abonamentului, până la webhooks ca sursă de adevăr, proration la schimbarea de plan, invoicing și recuperarea plăților eșuate.

Premisa: ai deja anatomia unei platforme SaaS clară și vrei să adaugi stratul de plată. Stripe e alegerea concretă aici — nu pentru că ar fi singura, ci pentru că API-ul și documentația scurtează drumul de la zero la primul abonament plătit. La final compari pe scurt cu un merchant of record, varianta în care renunți la control în schimbul conformității fiscale.

Checkout sau API direct: cât control vrei

Prima decizie nu e despre cod, ci despre cât din fluxul de plată vrei să gestionezi singur.

Stripe Checkout + Customer Portal (hosted)

Checkout e o pagină găzduită de Stripe. Redirectezi clientul acolo, el plătește, Stripe te anunță prin webhook. PCI compliance, 3D Secure (SCA), retry pe card, suport pentru zeci de metode de plată — toate vin din cutie. Customer Portal e geamănul lui: o pagină hosted unde clientul își schimbă planul, cardul sau anulează abonamentul, fără să scrii tu UI.

Trade-off-ul: livrezi rapid, dar UI-ul de plată trăiește pe domeniul Stripe, nu pe al tău.

1const session = await stripe.checkout.sessions.create({
2 mode: "subscription",
3 line_items: [{ price: "price_123", quantity: 1 }],
4 success_url: "https://app.exemplu.ro/billing?status=ok",
5 cancel_url: "https://app.exemplu.ro/billing?status=anulat",
6 customer: stripeCustomerId,
7});
8// redirect către session.url
1const session = await stripe.checkout.sessions.create({
2 mode: "subscription",
3 line_items: [{ price: "price_123", quantity: 1 }],
4 success_url: "https://app.exemplu.ro/billing?status=ok",
5 cancel_url: "https://app.exemplu.ro/billing?status=anulat",
6 customer: stripeCustomerId,
7});
8// redirect către session.url

API direct (Payment Intents + Subscriptions)

Dacă vrei UI-ul de plată în propria aplicație — Stripe Elements pentru câmpul de card, control complet pe flow — folosești direct Subscriptions API peste Payment Intents. Câștigi controlul, dar preiei și responsabilitatea: gestionezi tu starea incomplete când plata cere autentificare 3DS, retry-ul în interfață, edge case-urile.

Regula practică: pornește cu Checkout. Treci la API direct doar când ai un motiv concret de UX pentru care pagina hosted nu e suficientă.

Ciclul de viață al unui abonament

Un abonament Stripe nu e un boolean „plătit / neplătit". E o mașină de stări, și fiecare tranziție îți spune ce să faci în aplicație.

Diagramă cu ciclul de viață al unui abonament Stripe: de la Checkout, prin webhooks ca sursă de adevăr, la factură și ramura de dunning pentru plăți eșuate.

Stările care contează:

  • incomplete — abonamentul e creat, dar prima plată nu a trecut încă (de obicei așteaptă confirmarea 3DS). Expiră în 23 de ore dacă nu se confirmă.
  • trialing — perioadă de trial, fără plată încă.
  • active — plătit, totul în regulă. Aici dai acces la produs.
  • past_due — o plată a eșuat, Stripe reîncearcă. Clientul are de obicei încă acces, dar e pe muchie.
  • canceled / unpaid — după ce retry-urile s-au epuizat, în funcție de cum ai configurat dunning-ul.

Greșeala clasică: legi accesul la produs de „a dat clientul cardul o dată", nu de starea curentă a abonamentului. Sursa de adevăr pentru acces e statusul active/trialing, citit din ce-ți trimite Stripe — nu un flag pe care l-ai setat tu la checkout.

Webhooks ca sursă de adevăr

Aici e miezul. Plata e asincronă. Clientul închide tab-ul, cardul cere 3DS abia mâine, banca respinge la al doilea ciclu — toate astea se întâmplă departe de request-ul tău HTTP. Singura sursă de adevăr despre starea plății sunt evenimentele webhook pe care Stripe ți le trimite.

Evenimentele pe care le asculți aproape mereu:

  • checkout.session.completed — clientul a terminat Checkout.
  • customer.subscription.created / .updated / .deleted — schimbări de stare ale abonamentului.
  • invoice.paid / invoice.payment_failed — rezultatul fiecărui ciclu de facturare.

Idempotență (sau cum nu dublezi o lună de acces)

Stripe poate livra același eveniment de mai multe ori. Dacă procesezi invoice.paid de două ori fără protecție, riști să extinzi abonamentul cu două luni la o singură plată. Soluția: fiecare eveniment are un id unic (evt_...). Salvează-l înainte de a procesa și sari peste dacă l-ai mai văzut.

1// handler webhook (schiță)
2const event = stripe.webhooks.constructEvent(rawBody, sig, endpointSecret);
3
4const alreadyProcessed = await db.events.exists(event.id);
5if (alreadyProcessed) return res.status(200).end(); // idempotent
6
7await db.events.insert(event.id);
8switch (event.type) {
9 case "invoice.paid": /* extinde accesul */ break;
10 case "invoice.payment_failed": /* marchează past_due */ break;
11 // ...
12}
13res.status(200).end();
1// handler webhook (schiță)
2const event = stripe.webhooks.constructEvent(rawBody, sig, endpointSecret);
3
4const alreadyProcessed = await db.events.exists(event.id);
5if (alreadyProcessed) return res.status(200).end(); // idempotent
6
7await db.events.insert(event.id);
8switch (event.type) {
9 case "invoice.paid": /* extinde accesul */ break;
10 case "invoice.payment_failed": /* marchează past_due */ break;
11 // ...
12}
13res.status(200).end();

Două reguli care te scapă de incidente:

  • Verifică semnătura. constructEvent validează header-ul Stripe-Signature cu secretul endpoint-ului. Fără asta, oricine îți poate falsifica un invoice.paid.
  • Răspunde rapid cu 200. Stripe consideră orice non-200 un eșec și reîncearcă livrarea cu backoff timp de până la trei zile. Procesează greul (email, provisioning) într-un job de fundal, nu în handler-ul de webhook.

Acel „job de fundal" e exact tipul de muncă pe care o muți pe un worker care rulează în spate: reconciliere periodică, retry pe provisioning, sincronizare cu Stripe — ca handler-ul de webhook să rămână subțire și rapid.

Reconciliere

Webhook-urile se pot pierde — un deploy prost, un downtime de zece minute, un endpoint care a dat 500. Nu te baza doar pe ele. Rulează periodic o reconciliere: interoghează Stripe API pentru abonamentele tale și aliniază starea locală cu cea reală. E plasa de siguranță pentru evenimentele ratate.

Proration: ce se întâmplă la upgrade și downgrade

Clientul trece de pe planul de 29 $ pe cel de 99 $ în mijlocul lunii. Cât plătește? Stripe calculează automat proration: îți creditează timpul neconsumat pe planul vechi și-ți facturează proporțional planul nou.

Comportamentul se controlează cu proration_behavior:

  • create_prorations (default) — generează liniile de proration și le adaugă pe următoarea factură.
  • always_invoice — facturează imediat diferența.
  • none — fără proration; schimbarea se aplică de la ciclul următor.

Pentru un upgrade, always_invoice încasează diferența pe loc — bun pentru cash flow. Pentru un downgrade, mulți preferă none, ca reducerea să intre în vigoare la reînnoire, nu retroactiv. Nu există un răspuns universal; depinde de cum vrei să tratezi creditele.

Invoicing și facturare

La fiecare ciclu, Stripe generează o factură. Are și ea stările ei: draftopenpaid (sau void / uncollectible). Pentru abonamente standard, Stripe finalizează și încasează factura automat; nu trebuie să faci nimic manual.

Atenție la o capcană: factura Stripe e un document de plată, nu neapărat unul fiscal valid în jurisdicția ta. Dacă vinzi din România, factura fiscală și raportarea sunt o discuție separată — TVA, reverse charge la clienți business din UE, e-Factura prin SPV. Am detaliat fluxul în ghidul despre TVA și e-Factura pentru SaaS, iar conformitatea fiscală e un capitol pe care Stripe nu-l rezolvă singur.

Pentru calculul taxei, Stripe oferă Stripe Tax — potrivit paginii de pricing Stripe (2026), costă în jur de 0,5% per tranzacție pe care calculează taxa. Util, dar nu te scutește de înregistrarea și depunerea declarațiilor acolo unde ai obligații.

Dunning: recuperarea plăților eșuate

Plățile recurente eșuează des — card expirat, fonduri insuficiente, blocaj de la bancă. „Involuntary churn" (clienți pierduți nu pentru că au vrut, ci pentru că plata a picat) e adesea o felie serioasă din churn-ul total. Dunning e mecanismul prin care recuperezi aceste plăți.

Stripe oferă Smart Retries: reîncearcă plata eșuată pe un program optimizat (nu la intervale fixe, ci când șansa de succes e mai mare), trimite emailuri de dunning și, după ce retry-urile se epuizează, mută abonamentul în canceled sau unpaid, după setarea ta.

Ce ține de tine:

  • Decide ce se întâmplă cu accesul în past_due — îl tai imediat sau lași o perioadă de grație.
  • Pune emailurile de dunning să sune a om, nu a robot de colectare.
  • Tratează customer.subscription.deleted ca pe momentul real de pierdere a accesului.

Recuperarea câtorva procente de involuntary churn se vede direct în bugetul și ROI-ul unui SaaS — e venit pe care deja l-ai câștigat, doar că nu l-ai încasat.

Cât costă Stripe pentru billing recurent

Cifrele se schimbă, așa că verifică-le la sursă înainte de a-ți construi modelul. La momentul scrierii (pricing public Stripe, 2026):

  • Procesare card: standard 2,9% + 0,30 $ per tranzacție reușită în SUA; pentru carduri europene în Europa, în jur de 1,5% + 0,25 €. Cardurile internaționale adaugă circa 1,5%, iar conversia valutară încă circa 1%.
  • Stratul de Billing: Stripe Billing se adaugă peste taxa de procesare pentru abonamente. Analizele pe 2026 îl pun în jur de 0,5–0,7% din venitul recurent, în funcție de plan (Stripe a consolidat fostele tiere Starter/Scale). Important: e un cost peste procentul de procesare, nu în loc de el.
  • Stripe Tax: circa 0,5% per tranzacție pe care calculează taxa.

Pe scurt: pe un abonament de 100 $, plătești aproximativ 3,20 $ procesare plus stratul de Billing deasupra. Modelează asta în prețul tău — modelul de pricing pe care îl alegi (flat, tiered, per-seat sau usage) decide cât te doare comisionul — mai ales pe tichete mici, unde cei 0,30 $ ficși cântăresc disproporționat.

Când renunți la control: merchant of record

Stripe te lasă procesatorul, dar tu rămâi vânzătorul legal — și deci responsabil de TVA și raportare în fiecare jurisdicție unde ai clienți. Un merchant of record (MoR) răstoarnă relația: el devine vânzătorul, colectează și remite taxele global, absoarbe chargeback-urile și-ți plătește net.

Paddle și Lemon Squeezy (acum parte din Stripe) percep în jur de 5% + 0,50 $ all-in, gestionând TVA/sales tax în peste 200 de jurisdicții — potrivit paginilor lor de pricing pe 2026. Plătești mai mult, dar scapi de povara fiscală internațională. Stripe a intrat și el în zona asta cu Stripe Managed Payments, propria soluție de MoR.

Regula de decizie: dacă vinzi global și conformitatea fiscală îți mănâncă săptămâni pe trimestru, MoR-ul devine infrastructură, nu lux. Dacă ești concentrat pe câteva piețe și ai capacitatea fiscală internă, Stripe direct îți păstrează mai mult din venit și control complet pe billing.

Întrebări frecvente

Stripe Checkout sau API direct pentru un SaaS la început?

Checkout, aproape întotdeauna. Îți dă PCI, 3DS, retry și Customer Portal din cutie, ca să livrezi în zile, nu săptămâni. Treci la API direct doar când ai un motiv concret de UX.

De ce sunt webhook-urile „sursa de adevăr" și nu răspunsul de la Checkout?

Pentru că plata e asincronă: 3DS, retry și eșecuri la cicluri viitoare se întâmplă după ce request-ul tău s-a încheiat. Doar evenimentele webhook reflectă starea reală în timp, deci pe ele legi accesul la produs.

Cum evit să procesez același eveniment de două ori?

Idempotență pe event.id. Salvează ID-ul fiecărui eveniment înainte de procesare și sari peste dacă l-ai mai văzut. Verifică și semnătura Stripe-Signature, ca să nu accepți evenimente falsificate.

Ce se întâmplă când un client face upgrade la mijlocul lunii?

Stripe calculează proration: creditează timpul neconsumat pe planul vechi și facturează proporțional noul plan. Controlezi comportamentul cu proration_behavior — pe loc (always_invoice) sau de la ciclul următor (none).

Mă scutește Stripe de TVA și e-Factura în România?

Nu. Stripe procesează plata, dar tu rămâi vânzătorul legal, deci TVA, reverse charge UE și e-Factura prin SPV rămân responsabilitatea ta. Un merchant of record preia partea asta, în schimbul unui comision mai mare.

Cât pierd din involuntary churn și cum îl recuperez?

Plățile eșuate (card expirat, fonduri) pot fi o felie semnificativă din churn. Dunning-ul prin Smart Retries reîncearcă plata pe un program optimizat și trimite emailuri de recuperare, salvând venit pe care deja l-ai câștigat.

Construiește billing-ul corect de la prima versiune

Billing-ul recurent e mai puțin despre integrarea Stripe și mai mult despre mașina de stări din spate: webhook-uri idempotente, dunning care recuperează, acces legat de starea reală a abonamentului. Dacă construiești un produs SaaS și vrei stratul de billing și abonamente așezat corect din v1, începe cu Checkout, tratează webhook-urile ca sursă de adevăr și modelează costul de procesare în preț.

Distribuie
Andrei Badulescu
Despre autor

Andrei Badulescu

Fondator & Software Architect

Construiește sisteme B2B la BaseTech — ERP la comandă, platforme SaaS, agenți AI și arhitecturi programmatic SEO. Scrie despre deciziile tehnice din spatele lor: stack, trade-off-uri și ce ține la scară.

Vezi profilul autorului →
Continuă
Newsletter

Insights pentru companii
care construiesc

Articole noi despre ERP, AI, agenți și pSEO, direct pe email. Fără spam.