Manual Webhook
Ultima actualizare: 11 august 2026
La fiecare document nou trimitem un POST semnat la adresa ta. Nu trebuie să întrebi nimic la interval — afli în aceeași secundă în care am aflat și noi.
Inclus în planurile Platinum și Contabil, ca și API-ul. Adresele se configurează în Setări → Integrări, maximum 3 per cont, doar https.
Cum se activează
- Adaugi adresa în Setări → Integrări. Trebuie să fie https — un webhook cărăuș de documente fiscale peste http simplu ar anula orice altă precauție.
- Îți generăm un secret la adăugare. El este afișat lângă adresă și este cheia cu care verifici semnătura.
- Prima livrare pleacă la primul document nou găsit după adăugare. Nu retrimitem documentele de dinainte.
- Fiecare document produce câte un POST către fiecare adresă activă.
Emailul rămâne promisiunea; webhook-ul este un plus peste el. Dacă serverul tău e căzut o zi, alerta pe email a plecat oricum.
Cum arată cererea
| Antet | Valoare |
|---|---|
| Content-Type | application/json |
| X-AlerteSPV-Timestamp | Momentul semnării, în secunde Unix. |
| X-AlerteSPV-Signature | „sha256=" plus HMAC-SHA256 peste „timestamp.corp", cu secretul adresei. |
| User-Agent | AlerteSPV-Webhook/1 |
POST /webhook-ul-tau HTTP/1.1
Host: exemplu.ro
Content-Type: application/json
User-Agent: AlerteSPV-Webhook/1
X-AlerteSPV-Timestamp: 1786431338
X-AlerteSPV-Signature: sha256=6f1c0a…
{"event":"document.new", …}- „event" este deocamdată întotdeauna „document.new". Verifică-l oricum: alte evenimente vor folosi aceeași adresă.
- „summary" are exact forma din API și este null când documentul nu are un sumar extractibil.
- Corpul nu conține XML-ul. Îl iei cu „GET /api/v1/documents/{id}/xml", folosind „document.id" din payload.
Verificarea semnăturii
Semnătura este peste „timestamp.corp" — timestamp-ul, un punct, apoi corpul brut al cererii. Semnarea corpului singur ar permite oricui a văzut o cerere validă să o repete la nesfârșit; cu timestamp-ul înăuntru, tu poți respinge o cerere veche fără ca noi să ținem vreo stare.
Verifică peste corpul brut, exact octeții primiți. Dacă îl decodezi în obiect și îl re-serializezi, ordinea cheilor și escaparea se schimbă, iar semnătura nu se mai potrivește niciodată.
<?php
$secret = getenv('ALERTESPV_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ALERTESPV_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_ALERTESPV_SIGNATURE'] ?? '';
// Cererile vechi se resping: fereastra de cinci minute inchide reluarea.
if (! ctype_digit((string) $timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit('timestamp');
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('semnatura');
}
$payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
// Idempotent pe id: aceeasi livrare poate veni de doua ori.
// upsert($payload['document']['id'], $payload);
http_response_code(200);Ce trebuie să răspunzi
- Orice cod 2xx înseamnă livrat. 200 sau 204, nu contează.
- Orice altceva — 3xx, 4xx, 5xx — înseamnă eșuat și declanșează reîncercarea.
- Ai 10 secunde. Peste asta închidem conexiunea și tratăm livrarea ca eșuată.
- Pune munca într-o coadă și răspunde imediat. Un webhook care parsează XML sincron va depăși limita exact în ziua cu multe facturi.
Reîncercări și duplicate
Fiecare livrare are 4 încercări: imediat, apoi după aproximativ un minut, cinci minute și douăzeci și cinci de minute.
| Încercare | Când |
|---|---|
| 1 | Imediat ce documentul a fost salvat. |
| 2 | După 60 de secunde. |
| 3 | După încă 5 minute. |
| 4 | După încă 25 de minute. |
Aceeași livrare poate ajunge de două ori — de exemplu dacă serverul tău a primit-o și a răspuns cu întârziere. Fii idempotent pe „document.id": stochează-l ca unic și ignoră ce ai văzut deja.
Când oprim o adresă
După 20 de eșecuri consecutive adresa se dezactivează singură. Nu e curățenie de dragul curățeniei: o adresă care ne refuză de două săptămâni este o adresă pe care am bate la fiecare document, pentru fiecare cont care a configurat-o cândva și a uitat de ea.
- Ultima eroare și numărul de eșecuri consecutive sunt afișate lângă adresă, în Setări → Integrări.
- O livrare reușită resetează contorul la zero.
- O adresă dezactivată se reactivează ștergând-o și adăugând-o din nou. Secretul nou trebuie pus și la tine.
- Livrările din perioada în care adresa era oprită nu se recuperează.
Cum testezi înainte de producție
Adresa trebuie să fie https și accesibilă din internet, deci un „localhost" nu merge direct. Un tunel public către mașina ta de dezvoltare este cea mai simplă cale; alternativa este să reproduci local o cerere semnată și să îți testezi verificarea cu ea.
# Reproduce o livrare semnata, catre propriul endpoint.
SECRET="secretul-afisat-in-panou"
BODY='{"event":"document.new","document":{"id":8814}}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)
curl -s -X POST https://exemplu.ro/webhook/alertespv \
-H "Content-Type: application/json" \
-H "X-AlerteSPV-Timestamp: $TS" \
-H "X-AlerteSPV-Signature: sha256=$SIG" \
--data-raw "$BODY"- Verifică întâi că respingi o semnătură greșită. Un endpoint care acceptă orice nu se vede din exterior.
- Verifică apoi că respingi un timestamp vechi de o oră.
- Abia la final verifică drumul fericit.