Webhookuri și evenimente
Îți spunem noi când se întâmplă ceva, la adresa ta, cu semnătură.
Un webhook nou
În aplicație, la API și webhookuri, adaugi adresa serverului tău (doar https://, publică), alegi evenimentele și firmele. Primești un secret whsec_…, cu care verifici că cererile vin de la noi. Butonul de test trimite pe loc un eveniment webhook.test și îți arată răspunsul serverului tău.

Webhookurile sunt ale contului tău, ca și cheile: auzi doar firmele în care ești și doar cât timp ești în ele. Un cont are cel mult 20 de webhookuri.
Evenimentele
| type | Ce s-a întâmplat | Permisiune pentru GET /events |
|---|---|---|
document.issued | Un document a fost emis (factură, proformă sau stornare), din API, din aplicație sau dintr-o factură recurentă. | documents:read |
document.deleted | Un document a fost șters. | documents:read |
document.imported | O factură trimisă din alt program prin SPV a fost preluată (doar cele apărute în ultimele 3 zile). | documents:read |
document.paid | O factură a fost încasată integral. | payments:read |
efactura.submitted | ANAF a primit XML-ul (are index de încărcare) și îl procesează. | documents:read |
efactura.accepted | ANAF a validat factura. | documents:read |
efactura.rejected | ANAF a respins factura; erorile sunt în efactura.errors. | documents:read |
payment.created | O încasare a fost înregistrată. | payments:read |
payment.deleted | O încasare a fost ștearsă (sau chitanța ei anulată). | payments:read |
receipt.issued | O chitanță a fost emisă. | payments:read |
receipt.cancelled | O chitanță a fost anulată. | payments:read |
received_invoice.created | O factură nouă de la un furnizor a apărut în SPV. | received:read |
company.verified | Firma a devenit verificată (cineva a dovedit drepturi SPV pe codul ei). | documents:read |
company.merged | O copie a firmei a fost unită cu firma verificată; data.object are from și into. | documents:read |
{
"id": "evt_10422",
"object": "event",
"type": "efactura.accepted",
"company_id": 7,
"created_at": "2026-10-09T14:09:40+03:00",
"data": {
"object": {
"id": 812, "object": "document", "type": "invoice", "status": "issued", "number": "FCT 42",
"external_ref": "comanda-1042",
"efactura": { "applies": true, "status": "accepted", "upload_index": "5012345678", "errors": null, "scheduled_at": null, "anaf_id": null },
"…": "…"
}
}
}Documentele vin în data.object ca în liste: fără linii și încasări. Pentru ele, citești documentul după id.
Cererea pe care o primești
Un POST cu evenimentul în corp (JSON) și trei antete, după specificația Standard Webhooks:
| Antet | Ce este |
|---|---|
webhook-id | Id-ul evenimentului (evt_…), același la fiecare reîncercare. |
webhook-timestamp | Momentul trimiterii, în secunde Unix. |
webhook-signature | v1, urmat de semnătura HMAC-SHA256, în base64. După o schimbare de secret vin două semnături, separate prin spațiu, 24 de ore. |
Răspunde cu orice cod 2xx în cel mult 15 secunde. Fă munca grea după răspuns, dintr-o coadă: un răspuns lent sau un cod de eroare înseamnă reîncercare.
Verificarea semnăturii
Semnătura este HMAC-SHA256 peste {webhook-id}.{webhook-timestamp}.{corpul exact primit}, cu cheia dată de partea de după whsec_, decodată din base64. Verifică și momentul (cel mult 5 minute diferență), ca o cerere veche să nu poată fi repetată. Folosește corpul exact cum a venit, înainte de orice parsare JSON.
Node.js
import crypto from 'node:crypto';
export function verify(secret, headers, body) {
const id = headers['webhook-id'], ts = headers['webhook-timestamp'], sigs = headers['webhook-signature'];
if (!id || !ts || !sigs || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest();
return sigs.split(' ').some((s) => {
const [v, b64] = s.split(',');
const got = Buffer.from(b64 || '', 'base64');
return v === 'v1' && got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}
// Express: corpul brut, nu cel parsat
app.post('/webhooks/chappa', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(process.env.CHAPPA_WHSEC, req.headers, req.body)) return res.sendStatus(400);
res.sendStatus(200);
queue.push(JSON.parse(req.body));
});Python
import base64, hashlib, hmac, time
def verify(secret: str, headers, body: bytes) -> bool:
msg_id, ts, sigs = headers.get("webhook-id"), headers.get("webhook-timestamp"), headers.get("webhook-signature")
if not (msg_id and ts and sigs) or abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
mac = hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
expected = base64.b64encode(mac).decode()
return any(hmac.compare_digest(s[3:], expected) for s in sigs.split(" ") if s.startswith("v1,"))PHP
function verify(string $secret, array $h, string $body): bool {
$id = $h['webhook-id'] ?? ''; $ts = $h['webhook-timestamp'] ?? ''; $sigs = $h['webhook-signature'] ?? '';
if ($id === '' || $ts === '' || abs(time() - (int) $ts) > 300) return false;
$key = base64_decode(substr($secret, strlen('whsec_')));
$expected = base64_encode(hash_hmac('sha256', "$id.$ts.$body", $key, true));
foreach (explode(' ', $sigs) as $s) {
if (str_starts_with($s, 'v1,') && hash_equals($expected, substr($s, 3))) return true;
}
return false;
}
$ok = verify(getenv('CHAPPA_WHSEC'), array_change_key_case(getallheaders()), file_get_contents('php://input'));Bibliotecile Standard Webhooks (standardwebhooks pentru JavaScript, Python, PHP, Go, Java, C#, Ruby, Rust) fac aceeași verificare, dacă preferi una gata făcută.
Reîncercările
Dacă serverul tău nu răspunde cu 2xx în 15 secunde, reîncercăm după 1 minut, 5 minute, 30 de minute, 2 ore, 5 ore, 10 ore și 14 ore: opt încercări, în aproximativ 32 de ore. În aplicație vezi fiecare livrare, cu codul primit și răspunsul, și o poți retrimite.

O adresă care nu răspunde cu succes 5 zile la rând este oprită și primești un email; o repornești din aplicație.
Duplicate și ordine
- Același eveniment poate veni de două ori (de exemplu dacă răspunsul tău s-a pierdut). Ține minte
webhook-idși sari peste cele deja tratate. - Evenimentele pot sosi în altă ordine decât s-au întâmplat, mai ales după reîncercări. Când contează starea de acum, citește documentul prin API.
Fără webhook
GET /api/v1/events dă aceleași evenimente, din ultimele 30 de zile, pentru firmele cheii. Îl folosești în locul unui webhook, dacă serverul tău nu poate primi cereri, sau ca să recuperezi ce ai pierdut cât a fost oprit: ții minte ultimul id tratat și ceri mai departe cu starting_after.
Referința
Lista evenimentelor
GET /api/v1/events
Evenimentele ultimelor 30 de zile din firmele cheii, cele mai noi primele; fiecare tip doar cu permisiunea de citire a resursei lui. Aceleași obiecte ca la webhookuri: le poți citi periodic în loc de webhook sau ca să recuperezi ce ai pierdut.
| Parametru | Tip | Ce este |
|---|---|---|
company_id | integer | Ale unei firme. |
type | string | De un tip. |
limit | integer | Câte pe pagină, 1–100. Implicit 25. |
starting_after | string | next_cursor din pagina anterioară. |
Un eveniment
GET /api/v1/events/{event_id}
Evenimentul, după id (evt_…).
| Parametru | Tip | Ce este |
|---|---|---|
event_id în adresă | string | Id-ul evenimentului (evt_…). |
Răspuns 200: Event.
{
"id": "evt_10422",
"object": "event",
"type": "efactura.accepted",
"company_id": 7,
"created_at": "2026-10-09T14:09:40+03:00",
"data": {
"object": {
"id": 812, "object": "document", "type": "invoice", "status": "issued", "number": "FCT 42",
"external_ref": "comanda-1042",
"efactura": { "applies": true, "status": "accepted", "upload_index": "5012345678", "errors": null, "scheduled_at": null, "anaf_id": null },
"…": "…"
}
}
}Obiectul Event
Ce s-a întâmplat într-o firmă. Același obiect vine la webhookuri și din GET /events.
| Câmp | Tip | Ce este |
|---|---|---|
id | string | evt_…, unic; la webhookuri și în antetul webhook-id. |
object | event | Mereu event. |
type | string | Tipul (lista mai jos). |
company_id | integer | Firma. |
created_at | date-time | Când s-a întâmplat. |
data | object | object: documentul (ca în liste), încasarea, chitanța, factura primită sau firma, după tip. |