compliance

Integrare SmartBill cu Stripe: factură fiscală automată după plată

Webhook Stripe la SmartBill API, generare factură fiscală în e-Factura: arhitectură, cod Node.js și Spring Boot, idempotență, BNR, erori clasice.

Cuprins

Ghid practic pentru fluxul complet: client plătește prin Stripe, sistemul tău generează factura fiscală prin SmartBill API, factura ajunge automat în e-Factura ANAF. Arhitectura discutată funcționează pentru SaaS cu abonament, e-commerce, platforme de servicii și orice produs unde plata și facturarea trebuie să fie automate, sub control fiscal românesc2.

Combinația specifică Stripe plus SmartBill este populară pentru că Stripe acoperă plata internațională (card, Apple Pay, Google Pay, SEPA) iar SmartBill acoperă regimul fiscal românesc (numerotare conformă, raportare e-Factura, e-Transport pentru livrări fizice). Stripe Invoicing nu emite factură fiscală RO, deci nu poate înlocui SmartBill; explicăm de ce mai jos. Pentru contextul fiscal al e-Facturii, vezi entry-ul dedicat.

Cum arată arhitectura integrării Stripe-SmartBill?

Arhitectura leagă patru pași asincroni: plata prin Stripe, webhook-ul către endpoint-ul tău, coada internă și emiterea facturii prin SmartBill API, cu trimitere automată în e-Factura:

  1. Clientul plătește prin Stripe Checkout sau Payment Element. Stripe procesează plata, încarcă cardul, returnează succes.
  2. Stripe livrează un webhook (payment_intent.succeeded sau invoice.payment_succeeded pentru abonamenți) la endpoint-ul tău.
  3. Endpoint-ul tău verifică semnătura webhook-ului, înregistrează evenimentul în baza de date și pune un job în coadă pentru emiterea facturii.
  4. Job-ul de fundal apelează SmartBill API, primește factura emisă cu număr fiscal, apoi SmartBill o trimite automat în e-Factura.

Separarea între webhook receiver și emiterea facturii este crucială pentru robustețe. Webhook-ul Stripe trebuie să răspundă în maxim 30 de secunde, altfel Stripe declanșează retry. SmartBill API poate fi lent (sute de milisecunde tipic, dar până la câteva secunde la rate limit) și nu poate fi blocat de timeout-ul Stripe. Separarea în coadă rezolvă tensiunea.

Cum configurezi Stripe pentru facturare automată?

În Stripe configurezi trei lucruri: endpoint-ul de webhook, signing secret-ul și lista de evenimente la care te abonezi:

  • Endpoint webhook: URL public care primește POST-uri de la Stripe. Înregistrat în dashboard Stripe sau prin API la setup.
  • Signing secret: cheia secretă pe care Stripe o folosește pentru a semna webhook-urile (whsec_...). Stocată ca env var, niciodată în cod.
  • Lista de evenimente: pentru fluxul tipic, payment_intent.succeeded (plăți one-time) plus invoice.payment_succeeded și invoice.payment_failed (abonamenți).

În handler-ul webhook, verifică semnătura înainte de orice altceva. În Node.js, biblioteca stripe oficială expune stripe.webhooks.constructEvent(rawBody, signature, secret) care aruncă dacă semnătura nu se potrivește. În Spring Boot, folosește același pattern cu Webhook.constructEvent din SDK-ul Java oficial.

Cum configurezi SmartBill pentru emitere automată?

SmartBill API folosește autentificare Basic cu username (adresa de email a contului) și un token API generat din dashboard. Token-ul are durată practic nelimitată dar poate fi rotit, deci tratează-l ca un secret.

Endpoint-uri esențiale:

  • POST /SBORO/api/invoice: emiterea unei facturi noi.
  • GET /SBORO/api/invoice/status?cif=...&seriesName=...&number=...: verificarea statusului în e-Factura.
  • POST /SBORO/api/payment: înregistrarea unei plăți pe o factură existentă (util pentru proforma → fiscală).

Rate limit-ul SmartBill este de aproximativ 3 cereri pe secundă; depășirea returnează 403 (nu 429) pentru până la zece minute1. Clientul tău trebuie să implementeze rate limiting defensiv și să trateze 403 ca semnal de backoff, nu ca eroare permanentă.

Cum scrii handler-ul pentru webhook-ul Stripe?

Handler-ul face trei lucruri: verifică semnătura webhook-ului, înregistrează evenimentul și răspunde imediat, lăsând emiterea facturii pe seama unui job din coadă. În Node.js cu Express, structura tipică:

app.post('/webhooks/stripe',
  express.raw({type: 'application/json'}),
  async (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // Idempotency check
    const exists = await db.events.findById(event.id);
    if (exists) return res.json({received: true, dedupe: true});

    await db.events.insert({id: event.id, type: event.type, ts: Date.now()});
    await queue.publish('invoice-emit', { eventId: event.id, payload: event.data });
    res.json({received: true});
  }
);

Patru lucruri importante: verificarea semnăturii la început (orice eroare = 400 imediat), idempotență pe event.id înainte de a face muncă, înregistrarea evenimentului în baza de date înainte de a-l pune în coadă, răspuns rapid la Stripe (200 OK în sub o secundă).

Cum emiți factura prin SmartBill API?

În job-ul de fundal, după ce ai citit evenimentul Stripe din coadă, construiește payload-ul SmartBill:

const payload = {
  companyVatCode: 'RO12345678',
  client: {
    name: customer.name,
    vatCode: customer.vatCode,
    address: customer.address,
    isTaxPayer: !!customer.vatCode
  },
  issueDate: today,
  seriesName: 'FAC',
  isDraft: false,
  dueDate: addDays(today, 30),
  products: lineItems.map(li => ({
    name: li.description,
    code: li.sku,
    measuringUnitName: 'buc',
    currency: 'RON',
    quantity: li.quantity,
    price: li.unitPrice / 100,  // Stripe stochează în bani, SmartBill în lei
    isTaxIncluded: false,
    taxName: 'Normala',
    taxPercentage: 19
  })),
  payment: {
    value: amountPaid / 100,
    paymentSeries: 'STRIPE',
    type: 'Card',
    isCash: false
  }
};

try {
  const invoice = await smartbill.post('/SBORO/api/invoice', payload);
  await db.invoices.insert({
    eventId, smartbillNumber: invoice.number, status: 'issued'
  });
} catch (err) {
  if (err.status === 403) {
    await queue.delay('invoice-emit', { eventId }, 60_000);
    return;
  }
  throw err;
}

Trei capcane practice: prețurile Stripe sunt în bani (sutimi de unitate), SmartBill le vrea în lei. Conversia valutară Stripe → RON, când plata vine în EUR sau USD, trebuie făcută folosind cursul BNR din ziua tranzacției, nu cursul Stripe (cele două diferă cu fracțiuni de procent). Câmpul isTaxPayer pe client trebuie corect setat pentru ca SmartBill să aleagă schema de TVA corespunzătoare.

Cum verifici că factura a ajuns în e-Factura?

SmartBill primește factura, o emite cu număr fiscal, apoi o trimite în e-Factura ANAF. Trimiterea către ANAF este asincronă; nu primești confirmare instant. Configurezi un job care, după N minute (recomandat 5-10), verifică statusul prin endpoint-ul /invoice/status:

const status = await smartbill.get(`/SBORO/api/invoice/status`, {
  params: { cif: 'RO12345678', seriesName: 'FAC', number: invoice.number }
});
// status.efacturaStatus: 'pending' | 'sent' | 'accepted' | 'rejected'
if (status.efacturaStatus === 'rejected') {
  alertOnDiscord(`Factura ${invoice.number} respinsă ANAF: ${status.errorMessage}`);
}

Doar statusul accepted confirmă că obligația fiscală este îndeplinită. sent înseamnă că SmartBill a trimis-o, dar ANAF nu a răspuns încă (rar mai mult de o oră, dar posibil la încărcare mare). rejected cere intervenție: analiza erorii, corectarea facturii (probabil emiterea unei facturi corective), retransmiterea.

Care sunt erorile comune în integrarea Stripe-SmartBill?

Blocajele frecvente vin din config, nu din cod: webhook respins cu 400, SmartBill 403 persistent, factură emisă dar netrimisă în e-Factura și respingere ANAF pe CUI inactiv:

  • Webhook Stripe respins cu 400: signing secret greșit sau corpul cererii nu este raw. Verifică middleware-ul Express, nu trebuie să facă JSON parse înainte de Stripe handler.
  • SmartBill 403 persistent: ai depășit rate limit-ul și încerci prea repede. Crește backoff-ul la 60 secunde minim între retransmiteri.
  • Factură emisă dar fără e-Factura: verifică în SmartBill dashboard dacă opțiunea „trimite automat în e-Factura" este activă pentru seria FAC. Implicit este on, dar uneori este off pe conturile mai vechi.
  • ANAF respinge cu CUI inactiv: clientul tău are CUI suspendat sau radiat. Validează CUI-ul la rândul lui prin API-ul ANAF de verificare contribuabili înainte de a încerca emisia.

De ce crawlerra pentru integrarea Stripe-SmartBill?

Combinația Stripe plus SmartBill plus e-Factura este un proiect tipic de 2-3 săptămâni pentru un dezvoltator care a mai parcurs acest proces, cu toate capcanele cunoscute. Pe crawlerra construim astfel de pipeline-uri ca module reutilizabile: handler webhook tipizat cu idempotență built-in, client SmartBill cu rate limiting și retry-uri configurabile, job de status polling cu alertare către Discord la rejected. Pentru contextul de format al e-Facturii, vezi entry-ul despre UBL 2.1; pentru fluxul de proforma înainte de factura fiscală, vezi factura proforma; pentru semantica token-urilor folosite, vezi JWT.

  1. Caracteristicile rate limit-ului SmartBill observate în clientul nostru de producție: aproximativ 3 cereri pe secundă, depășirea returnează 403 (nu 429) pentru până la zece minute, fără headers care să declare limita. Clientul nostru tratează 403 ca semnal de rate limit, aplică un backoff de 60 secunde și retransmite o singură dată înainte de a propaga eroarea. [crawlerra.smartbill_backoff]
  2. Documentația SmartBill API este publicată la api.smartbill.ro; documentația e-Factura ANAF la efactura.anaf.ro/specificatii-tehnice. Combinația celor două, plus regulile Codului Fiscal pentru factură fiscală, constituie sursa de adevăr pentru orice implementare în producție. [seo.smartbill_efactura_docs]

Întrebări frecvente

De ce nu folosesc direct Stripe Invoicing fără SmartBill?

Stripe Invoicing nu emite factură fiscală conform Codului Fiscal românesc și nu o trimite în e-Factura. Generează un document PDF cu structură convenabilă, dar fără numerotare fiscală în seria companiei tale, fără raportare automată ANAF și fără semnătură conformă. Pentru piața românească, factura fiscală trebuie emisă printr-un sistem care respectă regimul fiscal RO (SmartBill, Oblio, FGO sau implementare proprie), iar Stripe rămâne doar la rolul de procesator de plată.

Cum gestionez webhook-urile duplicate de la Stripe?

Folosește identificatorul evenimentului (event.id) ca cheie de idempotență. Stripe livrează același webhook de mai multe ori în condiții de retry (network blip, 5xx temporar pe receiverul tău). Stochezi event.id într-o tabelă de evenimente procesate; la fiecare webhook, verifici dacă există deja înainte de a emite factura fiscală. Așa primești de două ori webhook-ul, dar emiți o singură factură.

Ce fac dacă SmartBill returnează 403 când vreau să emit factura?

SmartBill folosește 403 ca semnal de rate limit (nu 429), cu o fereastră de blocaj de până la zece minute. Implementarea defensivă tratează 403 ca rate limit, aplică un backoff de minim 60 secunde și retransmite o singură dată. Dacă și a doua încercare eșuează cu 403, escalează către coada de retry asincronă în loc să blochezi webhook-ul Stripe. Webhook-ul Stripe nu trebuie să aștepte minute pentru răspunsul SmartBill; răspunde rapid la Stripe și fă apelul SmartBill într-un job de fundal.

Ce status confirmă că factura a fost acceptată în e-Factura?

Statusul „acceptat ANAF" este singurul care confirmă că obligația fiscală a fost îndeplinită. Emisă în SmartBill nu înseamnă automat acceptată în e-Factura: trimiterea către ANAF este asincronă, iar SmartBill expune un endpoint de status care arată dacă factura a fost trimisă și care a fost răspunsul ANAF. Dacă trimiterea a eșuat sau ANAF a respins factura, vrei să afli dintr-o alertă (Sentry, Discord), nu de la client.