FacturareIntră în aplicație
Integrare în aplicația taAPI-ul iframe-ului · pagina 4 din 10

RO e-Factura în aplicația ta, într-o oră

Nu scrii XML și nu vorbești cu ANAF. Pui Chappa Facturare într-un iframe, iar utilizatorii tăi emit facturi, le trimit în SPV și văd facturile primite fără să iasă din aplicația ta.

Ora, pas cu pasDirect la referință

Autentificarea rămâne a ta: serverul tău semnează o aserțiune ES256 despre utilizatorul logat. Nu există niciun apel de la serverul tău la noi.

<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="…" xmlns:cbc="…">
  <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</cbc:CustomizationID>
  <cbc:ID>FCT0042</cbc:ID><cbc:IssueDate>2026-10-08</cbc:IssueDate><cbc:DueDate>2026-10-23</cbc:DueDate>
  <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode><cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>
  <cac:AccountingSupplierParty><cac:Party>
    <cac:PostalAddress><cbc:StreetName>Str. Unirii 1</cbc:StreetName><cbc:CityName>Cluj-Napoca</cbc:CityName><cbc:CountrySubentity>RO-CJ</cbc:CountrySubentity></cac:PostalAddress>
    <cac:PartyTaxScheme><cbc:CompanyID>RO12345678</cbc:CompanyID><cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:PartyTaxScheme>
    <cac:PartyLegalEntity><cbc:RegistrationName>Alfa SRL</cbc:RegistrationName><cbc:CompanyID>J12/34/2010</cbc:CompanyID></cac:PartyLegalEntity>
  </cac:Party></cac:AccountingSupplierParty>
  <cac:AccountingCustomerParty><cac:Party>
    <cac:PostalAddress><cbc:CityName>Sector 3</cbc:CityName><cbc:CountrySubentity>RO-B</cbc:CountrySubentity></cac:PostalAddress>
    <cac:PartyTaxScheme><cbc:CompanyID>RO18547290</cbc:CompanyID></cac:PartyTaxScheme>
    <cac:PartyLegalEntity><cbc:RegistrationName>Beta SRL</cbc:RegistrationName></cac:PartyLegalEntity>
  </cac:Party></cac:AccountingCustomerParty>
  <cac:TaxTotal><cbc:TaxAmount currencyID="RON">52.50</cbc:TaxAmount>
    <cac:TaxSubtotal><cbc:TaxableAmount currencyID="RON">250.00</cbc:TaxableAmount><cbc:TaxAmount currencyID="RON">52.50</cbc:TaxAmount>
      <cac:TaxCategory><cbc:ID>S</cbc:ID><cbc:Percent>21</cbc:Percent><cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme></cac:TaxCategory></cac:TaxSubtotal></cac:TaxTotal>
  <cac:LegalMonetaryTotal><cbc:LineExtensionAmount currencyID="RON">250.00</cbc:LineExtensionAmount><cbc:TaxExclusiveAmount currencyID="RON">250.00</cbc:TaxExclusiveAmount>
    <cbc:TaxInclusiveAmount currencyID="RON">302.50</cbc:TaxInclusiveAmount><cbc:PayableAmount currencyID="RON">302.50</cbc:PayableAmount></cac:LegalMonetaryTotal>
  <cac:InvoiceLine><cbc:ID>1</cbc:ID><cbc:InvoicedQuantity unitCode="H87">1</cbc:InvoicedQuantity><cbc:LineExtensionAmount currencyID="RON">250.00</cbc:LineExtensionAmount>
    <cac:Item><cbc:Name>Abonament lunar</cbc:Name><cac:ClassifiedTaxCategory><cbc:ID>S</cbc:ID><cbc:Percent>21</cbc:Percent></cac:ClassifiedTaxCategory></cac:Item>
    <cac:Price><cbc:PriceAmount currencyID="RON">250.00</cbc:PriceAmount></cac:Price></cac:InvoiceLine>
</Invoice>
Ce trimiți tu, un mesaj
{ …, "type": "newInvoice", "payload": {
  "client": { "name": "Beta SRL", "cui": "RO18547290",
    "city": "Sector 3", "county": "București" },
  "lines": [{ "name": "Abonament lunar",
    "quantity": 1, "unitPrice": 250, "vatRate": 21 }]
} }
Ce facem noi cu restul
Numărul din serie, datele, tipul și moneda
Furnizorul, din datele firmei tale
TVA-ul pe fiecare cotă
Totalurile, rotunjite cum le verifică ANAF
Validarea după regulile CIUS-RO
Trimiterea în SPV și răspunsul ANAF
Ce primești înapoi
document.accepted  { number: 'FCT0042' }
Aceeași factură, din partea ta: un mesaj, cu clientul și liniile. XML-ul de deasupra îl construim, îl validăm și îl trimitem noi.

Ora, pas cu pas

Șase pași, în ordinea în care îi faci. Timpii sunt ai cuiva care citește documentația prima oară și are la îndemână certificatul digital al unei firme.

  1. 0 min

    Cheile

    O pereche de chei EC P-256, generată pe serverul tău. Cheia privată nu pleacă de acolo; cea publică merge în fiecare aserțiune.

    openssl ecparam -name prime256v1 -genkey -noout -out chappa-private.pem
  2. 5 min

    Aserțiunea, pe serverul tău

    Un endpoint care, pentru utilizatorul logat, semnează cine este și ce firme vede. Ai exemplul gata scris în Node.js, Python, PHP și C#, fără biblioteci externe.

  3. 20 min

    Iframe-ul

    Iframe-ul cu ?api. Când îți trimite ready, îi răspunzi cu init și aserțiunea. La prima aserțiune validă integrarea apare singură, în mod test, fără cont la noi.

    { "type": "init", "payload": { "assertion": "eyJhbGci…" } }
  4. 30 min

    Prima factură

    Utilizatorul o completează în iframe, sau o precompletezi tu cu mesajul newInvoice, din datele pe care le ai deja.

  5. 40 min

    Firma, conectată la ANAF

    Cine are certificatul digital al firmei autorizează accesul o singură dată, din aplicația ta. Dacă certificatul îl are contabilul, îi trimiți un link.

  6. 55 min

    Prima factură în SPV

    Factura pleacă în SPV, ANAF o validează, iar pagina ta primește evenimentul document.accepted.

Referința

Comenzile, evenimentele și erorile, în mesaje JSON prin postMessage.

Actualizat la

Cum vorbești cu iframe-ul

Pagina ta și iframe-ul își trimit mesaje JSON prin postMessage, ca cererile și răspunsurile unui API HTTP. Tu trimiți cereri (produsele tale, o factură precompletată, emiterea, PDF-ul) și iframe-ul răspunde la fiecare; când se întâmplă ceva (factură emisă, validată de ANAF, încasată), îți trimite un eveniment. Le trimiți și le citești cum vrei, din orice framework.

Adresa iframe-ului are ?api. Un schimb întreg, de la deschidere până la factura validată (→ trimiți tu, ← primești):

← { "chappa": "facturare/1", "kind": "event", "type": "ready", "payload": { "protocol": "facturare/1" } }

→ { "chappa": "facturare/1", "kind": "request", "id": "1", "type": "init",
    "payload": { "assertion": "eyJhbGciOiJFUzI1NiIs…" } }
← { "chappa": "facturare/1", "kind": "response", "id": "1",
    "payload": { "ok": true, "result": { "mode": "integrator", "user": { … }, "entities": [ … ], … } } }

→ { "chappa": "facturare/1", "kind": "request", "id": "2", "type": "newInvoice",
    "payload": { "client": { "name": "Beta SRL", "cui": "RO18547290", "city": "Sector 3", "county": "București" },
                 "lines": [{ "name": "Abonament lunar", "quantity": 1, "unitPrice": 250, "vatRate": 21 }] } }
← { "chappa": "facturare/1", "kind": "response", "id": "2",
    "payload": { "ok": true, "result": { "documentId": 812 } } }

← { "chappa": "facturare/1", "kind": "event", "type": "document.accepted",
    "payload": { "id": 812, "number": "FCT0042", "stage": "accepted", … } }

Trimiți cu iframe.contentWindow.postMessage(mesaj, 'https://facturare.chappa.ro') și primești în addEventListener('message', …). Regulile sunt la mesaje; cererile, evenimentele și erorile au câte un tabel.

Fără assertion în init, utilizatorii intră cu contul Chappa, ca la iframe-ul simplu; atunci pagina ta poate doar asculta evenimentele și schimba ecranul. view din init alege ecranul de pornire, iar theme aspectul.

Endpointul de aserțiuni

Pagina ta cere aserțiunea de la propriul server, în sesiunea utilizatorului tău. Reguli:

  • Doar pentru utilizatorul autentificat în aplicația ta, cu sub luat din sesiune, niciodată din parametri trimiși de browser.
  • POST + verificarea originii (sau tokenul CSRF pe care îl folosești deja). Fără CORS: alte site-uri nu au ce citi.
  • Răspuns text/plain cu aserțiunea, Cache-Control: no-store.
  • Expirare scurtă (2 până la 5 minute) și un jti nou de fiecare dată. Când sesiunea din iframe expiră, iframe-ul trimite assertion_required, iar pagina ta răspunde cu authenticate și o aserțiune nouă.
// Express, exemplu
app.post('/chappa/assertion', requireLogin, (req, res) => {
  if (req.get('Origin') !== 'https://app.exemplu.ro') return res.sendStatus(403);
  res.set('Cache-Control', 'no-store').type('text/plain')
     .send(chappaAssertion(req.user, 'https://app.exemplu.ro'));
});

Mesajele

Plicul, în ambele sensuri:

{ "chappa": "facturare/1", "kind": "request" | "response" | "event", "id": "…", "type": "…", "payload": { … } }
  • Iframe-ul are adresa https://facturare.chappa.ro/embed/?api: ?api îi spune că pagina ta vorbește cu el. Când e gata, trimite ready (cu targetOrigin *, doar cu versiunea protocolului).
  • Ascultă mesajele înainte să pui adresa iframe-ului, ca să nu pierzi ready.
  • Prima cerere trebuie să fie init; ce trimiți cât timp rulează init se tratează după el, în ordine. Originea paginii care o trimite devine fixă: iframe-ul ignoră orice mesaj din altă origine sau de la altă fereastră decât window.parent și răspunde doar acelei origini.
  • Tu verifici la rândul tău: event.source === iframe.contentWindow și event.origin === 'https://facturare.chappa.ro'; trimite cu targetOrigin https://facturare.chappa.ro.
  • id îl alegi tu, unic pentru fiecare cerere. Răspunsul: { "ok": true, "result": … } sau { "ok": false, "error": { "code", "message", "details"? } }, cu același id.

Cereri

typepayloadresult
init{assertion?, theme?, view?}{mode, user, integration, entities, capabilities, scopes, ignoredTheme}
authenticate{assertion}ca la init
setThemetema întreagă (aspectul): culori de zi și de noapte, forme, font, navigare, logo{theme, ignored}
logoutnimic{ok}
navigate{view, id?, tab?, entity?}, cu view una dintre: dashboard, invoices, invoice, new-invoice, new-proforma, received, payments, clients, products, settings, anaf{view}
selectEntity{ref} sau {entityId}{entityId}
setProducts{products:[{ref, name, code, description, unit, price, priceIncludesVat, vatRate, vatCategory, currency}], persist?}{count, persisted}
newInvoice{type?, entity?, client:{kind, name, cui|cnp, sendCnp, regCom, address, city, county, country, email, ref}, lines:[{name, description, code, unit, quantity, unitPrice, currency?, priceIncludesVat, discountPercent, vatRate, vatCategory}], currency, issueDate, dueDate, notes, orderRef, contractRef, externalRef, paymentMethod, paid?, open?}{documentId}
getDocument{id}rezumatul documentului (număr, stare, totaluri, plată, e-Factura, acțiuni permise)
listDocuments{q, from, to, stage, paid, type, limit}{documents}
issueDocument{id, seriesId?}rezumatul documentului
submitDocument{id}rezumatul documentului
getDocumentPdf{id}{url, expiresIn}
startAnafAuthorization{entity?, returnUrl?}{url, mode:'redirect'} sau {mode:'ui'}
createAnafAuthorizationLink{entity?}{url, expiresInDays}

Unitățile de măsură sunt coduri UN/ECE (H87 bucată, HUR oră, DAY, MON, KGM, LTR, MTR, C62…). Cotele de TVA: vatRate în procente (21, 11); categoriile: S standard, Z cotă zero, E scutit, AE taxare inversă, K intracomunitar, G export.

O linie cu preț în altă monedă primește currency (de exemplu un serviciu în EUR pe o factură în lei): o transformăm la cursul BNR valabil la data facturii, iar pe factură apare prețul în moneda ei. La o factură recurentă, conversia se face din nou la fiecare dată.

Încasarea la emitere: paid: true marchează factura ca încasată integral, prin paymentMethod (bon_fiscal, numerar, card, ordin_plata, cec, altul). La emitere se înregistrează plata; numerar emite și chitanța, iar bon_fiscal trimite factura în RO e-Factura ca factură pe bon fiscal (tip 751). Fără paid, paymentMethod este modul de plată așteptat.

Evenimente

typepayload
ready{protocol}
assertion_required{reason}; trimite authenticate cu o aserțiune nouă
auth_changed{user, integration}
resize{height}
navigated, entity_changedecranul / firma curentă
document.created, document.updated, document.issued, document.deleted, document.submitted, document.accepted, document.rejected, document.paid{id, …}
anaf.connected{entity, name, kind, cif, joined}: firma conectată (joined: utilizatorul a intrat în firma verificată a codului fiscal)
registration_required{reason, limits, usage, portal}
error{code, message}

Erori

codeCând
assertion_invalidSemnătură, algoritm, antet sau câmpuri greșite (mesajul spune ce).
assertion_expiredexp depășit.
assertion_replayedAcelași jti a doua oară.
unknown_key / key_revokedDoar kid pentru o cheie necunoscută / cheie revocată.
origin_mismatchPagina nu corespunde lui ori sau originilor înregistrate.
integration_blockedIntegrare blocată.
registration_requiredS-au atins limitele modului test.
not_authenticated, forbidden, not_found, conflict, not_allowed_in_state, bad_requestCereri invalide sau nepermise (ex. ștergerea unei facturi deja trimise la ANAF).
anaf_not_connectedFirma nu are conexiune ANAF.
rate_limited, server_error, timeoutReîncearcă mai târziu.