Webhook o doručení: ako ho prijať správne
eP24 vám pošle podpísanú udalosť vtedy, keď faktúra naozaj dorazí k odberateľovi (outbox.document.delivered), alebo keď sa odoslanie definitívne skončí neúspechom (outbox.document.failed). Prijať ju je pár riadkov kódu. Prijať ju správne stojí na dvoch veciach, na ktorých padne väčšina integrácií — a obe zlyhávajú tak, že to vyzerá na niečo iné.
1. Podpis počítajte nad surovým telom. Vždy.
Váš framework telo požiadavky ochotne rozparsuje a ponúkne vám pekný objekt. Keď z toho objektu vyrobíte JSON späť a podpíšete ten, podpis nebude sedieť nikdy — a chyba bude vyzerať ako zlý kľúč. Poradie kľúčov, medzery, spôsob zápisu čísel aj unicode escapy sa pri serializácii menia; HMAC je funkcia bajtov, nie významu.
// ZLE — telo prešlo parserom a späť: iné bajty, podpis nikdy nesedí
const event = await request.json();
const expected = hmac(secret, ts + "." + JSON.stringify(event));
// DOBRE — presne tie bajty, ktoré prišli
const raw = await request.text();
const expected = hmac(secret, ts + "." + raw);
const event = JSON.parse(raw); // až po overeníV praxi to znamená: v Next.js request.text(), v ASP.NET Core čítanie req.Body pred model bindingom, v PHP file_get_contents("php://input"), v Django request.body (nie request.POST), v Spring @RequestBody byte[]. Telo parsujte až po overení podpisu.
2. Deduplikácia nie je voliteľná
„Aspoň raz“ nie je poznámka pod čiarou. Ak vaša odpoveď dorazí neskoro alebo sa cestou stratí, eP24 to počíta ako neúspech a pošle tú istú udalosť znova — aj keď ste ju už uložili a zaúčtovali. ERP, ktorý účtuje pri príchode udalosti, zaúčtuje dvakrát. Pri prijatých faktúrach to znamená aj druhý záväzok v účtovníctve, nielen riadok v logu.
Ochrana je jedno pole: X-EP24-Delivery je pri každom opakovaní tej istej udalosti rovnaké. Uložte ho s unikátnym indexom a v tej istej transakcii ako zmenu stavu. Nie „pozri, či už existuje, potom zapíš“ — to sú dve operácie a medzi nimi sa zmestí druhé doručenie.
-- Jedna transakcia: buď je udalosť zapísaná aj zaúčtovaná, alebo nič.
begin;
insert into webhook_deliveries (delivery_id, received_at)
values ($1, now())
on conflict (delivery_id) do nothing; -- druhý pokus sa ticho zahodí
-- Ak insert nič nevložil, udalosť sme už spracovali: commit a 200.
update documents
set delivery_state = $2, delivered_at = $3
where id = $4 and (delivered_at is null or delivered_at < $3);
commit;Duplikát je úspech: odpovedzte 2xx. Keď odpoviete chybou, eP24 to skúsi znova a znova, a nakoniec vám endpoint vypne.
Tri veci, na ktoré sa zabúda o niečo menej často
- Najprv časová pečiatka, potom digest. Prehratá stará udalosť má úplne platný podpis — zastaví ju len okno
±300 sokoloX-EP24-Timestamp. Do digestu dávajte reťazec z hlavičky tak, ako prišiel, nie číslo, ktoré ste z neho vyrobili. - Skúšajte všetky prvky
v1=. Hlavička je zoznam. Počas rotácie kľúča v nej eP24 posiela 24 hodín dva podpisy — starým aj novým kľúčom. Kto berie prvý prvok, prežije ten deň na401; kto skúša všetky, nezbadá nič. 401a503nie sú to isté.401znamená „tento podpis je zlý“ a eP24 ho počíta medzi odmietnutia, ktoré endpoint po 20 pokusoch vypnú.503znamená „teraz neviem overiť, spýtaj sa znova“ — presne to, čo chcete odpovedať v deň, keď ešte nemáte nastavený kľúč.
3. Prijaté faktúry nesú celé XML
Tretia udalosť, inbox.document.received, ide opačným smerom: faktúra prišla vám, a UBL cestuje priamo v tele udalosti — nie za druhým volaním. Pre prijímač to znamená tri veci navyše.
Telo býva rádovo väčšie. Medián je 4,4 kB, 95. percentil 90 kB, faktúry s prílohami výrazne viac; eP24 garantuje strop 4 MB na celé telo udalosti. Limit, ktorý vás zastaví najskôr, je predvolených 100 kB v express.json(). Ten 95. percentil síce prejde, ale len o desatinu — prvá faktúra s prílohou tú rezervu minie a vznikne 413, ktoré žiadne opakovanie nevyrieši. Zdvihnite ho a rátajte s tým, že opakovanie pošle celé telo znova.
payload môže byť null. Dokument bol priveľký alebo sa ho nepodarilo načítať; payloadOmittedReason povie ktoré. Metadáta a odkaz do eP24 prídu tak či tak — faktúru teda nezahoďte, ukážte ju človeku. Toto je vetva, ktorú integrácie zabúdajú, a jej cena je ticho chýbajúca faktúra.
Overte payloadSha256. Podpis hlavičky dokazuje, že udalosť dorazila nezmenená. Tento digest dokazuje niečo iné: že XML vnútri je ten dokument, ktorý eP24 myslela. Keď nesedí, odpovedzte 2xx — bajty boli podpísané, opakovanie nič nezmení — a faktúru označte na overenie.
const event = JSON.parse(raw);
if (event.event === "inbox.document.received") {
const xml = event.received?.payload ?? null;
if (xml === null) {
// Faktúru NEZAHOĎTE: metadáta aj odkaz prišli, chýba len dokument.
await ulozNaRucneSpracovanie(event, event.payloadOmittedReason);
} else if (sha256(xml) !== event.payloadSha256) {
await oznacNaOverenie(event); // potvrdíme 2xx, ale neúčtujeme
} else {
await zalozPrijatuFakturu(xml, event.received.metadata);
}
}Peniaze v udalosti nie sú
Udalosť hovorí, čo sa stalo s dokumentom, nie čo to stálo — sumy ani identifikátory z peňaženky eP24 von neposiela. Na účtovanie to stačí, lebo vzťah je jednoznačný: jedno doručenie = jeden poplatok za cenu platnú v tej chvíli, a failed neznamená poplatok nikdy (eP24 účtuje až pri doručení, takže neúspešné odoslanie nebolo nikdy zaťažené a niet čo refundovať). Čo z udalosti neurobíte, je pomenovanie konkrétneho riadku v peňaženke; ten je vo výpise v portáli.
Vyskúšajte si, že vám to odmieta
Overovač, ktorý nikto nikdy nevidel niečo odmietnuť, je len nádej. Skript nižšie pošle proti vášmu endpointu zmenené telo aj hodinu starú pečiatku — obe musia skončiť na 401 — a rotáciu s dvoma podpismi, ktorá musí prejsť. Výsledok potom uvidíte v paneli simulátora.
EP24_WEBHOOK_SECRET=whsec_… npm run test:webhook -- https://vas-erp.exampleDeväť požiadaviek, z toho tri, ktoré musia zlyhať. Zdroj: webhook-selftest.mjs, overovač je lib/ep24-webhook.ts a celý prijímač route.ts — sú krátke naschvál, aby sa dali prečítať celé.
Úplný kontrakt — hlavičky, schéma udalosti, plán opakovaní, prahy vypnutia a overenie v PHP aj C# — je v príručke, kapitola 8a.