Referința
Comenzile, evenimentele și erorile, în mesaje JSON prin postMessage.
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
subluat 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/plaincu aserțiunea,Cache-Control: no-store. - Expirare scurtă (2 până la 5 minute) și un
jtinou de fiecare dată. Când sesiunea din iframe expiră, iframe-ul trimiteassertion_required, iar pagina ta răspunde cuauthenticateș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, trimiteready(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ăinitse 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âtwindow.parentși răspunde doar acelei origini. - Tu verifici la rândul tău:
event.source === iframe.contentWindowșievent.origin === 'https://facturare.chappa.ro'; trimite cu targetOriginhttps://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șiid.
Cereri
| type | payload | result |
|---|---|---|
init | {assertion?, theme?, view?} | {mode, user, integration, entities, capabilities, scopes, ignoredTheme} |
authenticate | {assertion} | ca la init |
setTheme | tema întreagă (aspectul): culori de zi și de noapte, forme, font, navigare, logo | {theme, ignored} |
logout | nimic | {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
| type | payload |
|---|---|
ready | {protocol} |
assertion_required | {reason}; trimite authenticate cu o aserțiune nouă |
auth_changed | {user, integration} |
resize | {height} |
navigated, entity_changed | ecranul / 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
| code | Când |
|---|---|
assertion_invalid | Semnătură, algoritm, antet sau câmpuri greșite (mesajul spune ce). |
assertion_expired | exp depășit. |
assertion_replayed | Același jti a doua oară. |
unknown_key / key_revoked | Doar kid pentru o cheie necunoscută / cheie revocată. |
origin_mismatch | Pagina nu corespunde lui ori sau originilor înregistrate. |
integration_blocked | Integrare blocată. |
registration_required | S-au atins limitele modului test. |
not_authenticated, forbidden, not_found, conflict, not_allowed_in_state, bad_request | Cereri invalide sau nepermise (ex. ștergerea unei facturi deja trimise la ANAF). |
anaf_not_connected | Firma nu are conexiune ANAF. |
rate_limited, server_error, timeout | Reîncearcă mai târziu. |