Namiesto opakovaného dotazovania nechajte platformu, nech vám zavolá sama.
Registrácia
curl -X POST https://testpostman.slovakodata.com/api/v1/webhooks \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.mojafirma.sk/hooks/epostar",
"events": ["invoice.delivered", "invoice.rejected", "invoice.accepted"]
}'
# 201 Created — secret vygeneruje platforma, v odpovedi príde len raz
{
"id": "b7e2f9a1-3c4d-4e11-9a2b-7f6c1d8e0a35",
"url": "https://erp.mojafirma.sk/hooks/epostar",
"events": ["invoice.delivered", "invoice.rejected", "invoice.accepted"],
"secret": "whsec_...",
"is_active": true,
"created_at": "2026-07-13T09:10:00.000Z"
}
Ako events uveďte zoznam konkrétnych udalostí z katalógu nižšie. Sekret na
podpisovanie doručovaných správ vygeneruje platforma a vráti ho len v tejto
odpovedi na vytvorenie — pri neskoršom zozname webhookov už nikde nie je.
Uložte si ho hneď; ak ho stratíte, treba webhook zmazať a založiť nový.
URL musí začínať https:// a smerovať na verejne dostupný server. Platforma
odmietne s chybou 400 adresu bez https, s menom a heslom v URL, jednoslovný
názov hosta (napr. https://erp/hook), interné domény (localhost, .local,
.internal) aj IP adresy z privátnych a rezervovaných rozsahov. To isté overuje
pri každom doručení: ak sa meno hosta preloží na takúto adresu, správa sa
nedoručí a neopakuje. Presmerovania (3xx) platforma nenasleduje.
Tvar správy
{
"event": "invoice.delivered",
"timestamp": "2026-07-13T09:15:03.871Z",
"data": {
"invoice_id": "9f2c1e84-3b7a-4d16-b0c5-1e8a72d40f31",
"mls_c3_status": null,
"mls_c5_status": null,
"status": "DELIVERED",
"selfBilling": false,
"invoiceTypeCode": "380",
"document_type": "INVOICE",
"invoice_number": "2026001",
"supplier_name": "Dodávateľ s.r.o.",
"buyer_name": "Odberateľ a.s.",
"total_amount": "120.00",
"currency": "EUR",
"issue_date": "2026-08-23",
"document_url": "https://postman.slovakodata.com/api/v1/invoices/9f2c1e84-3b7a-4d16-b0c5-1e8a72d40f31/xml"
}
}
# Hlavičky
X-Webhook-Signature: 4f1c0a... # HMAC-SHA256, hex
X-Webhook-Timestamp: 2026-07-13T09:15:03.871Z
Overenie podpisu
Podpis počítame z reťazca {timestamp}.{telo} pomocou HMAC-SHA256 a vášho
secretu. Vždy ho overte — inak vám ktokoľvek môže podstrčiť falošnú
udalosť. Na porovnanie použite funkciu odolnú voči časovej analýze.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isValidWebhook(
rawBody: string, // telo PRESNE tak, ako prišlo — nie re-serializované
signature: string, // X-Webhook-Signature
timestamp: string, // X-Webhook-Timestamp
secret: string,
): boolean {
// Staršie ako 5 minút zahodíme — ochrana proti prehratiu.
// Hlavička je ISO 8601, nie epoch: Number() by nad ňou dal NaN a porovnanie
// by vždy prešlo, takže by ochrana ticho nefungovala.
const sentAt = Date.parse(timestamp);
if (Number.isNaN(sentAt) || Math.abs(Date.now() - sentAt) > 300_000) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}using System.Globalization;
using System.Security.Cryptography;
using System.Text;
static bool IsValidWebhook(
string rawBody, string signature, string timestamp, string secret)
{
// Staršie ako 5 minút zahodíme — ochrana proti prehratiu.
if (!DateTimeOffset.TryParse(timestamp, CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind, out var sentAt)) return false;
if (Math.Abs((DateTimeOffset.UtcNow - sentAt).TotalMilliseconds) > 300_000) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var payload = Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}");
var expected = Convert.ToHexString(hmac.ComputeHash(payload)).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature));
}function isValidWebhook(
string $rawBody,
string $signature,
string $timestamp,
string $secret
): bool {
// Staršie ako 5 minút zahodíme — ochrana proti prehratiu.
$sentAt = strtotime($timestamp);
if ($sentAt === false || abs(time() - $sentAt) > 300) {
return false;
}
$expected = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secret);
return hash_equals($expected, $signature);
}Doručovanie a opakovanie
Čakáme na odpoveď najviac 30 sekúnd. Odpovedzte 2xx hneď, ako správu
prijmete, a spracujte ju až potom na pozadí. Ak endpoint zlyhá alebo
neodpovie, doručenie zaradíme na opakovanie.
Opakované pokusy sú podpísané rovnako ako prvý — rovnaký recept, rovnaký
tvar hlavičiek. Líšia sa len v timestamp: každý pokus nesie čerstvý čas, ktorý
je zároveň v podpísanom tele, takže päťminútové okno platí od daného pokusu, nie
od pôvodnej udalosti. Overovanie napísané podľa prvého doručenia teda platí pre
všetky pokusy.
Katalóg udalostí
| Udalosť | Kedy príde |
|---|---|
invoice.delivered | Prístupový bod odberateľa prijal odchádzajúcu faktúru a potvrdil to kladným AS4 potvrdením o prevzatí (receipt): status: "DELIVERED", mls_c3_status: null. Faktúra je odoslaná, doručenie ešte nie je potvrdené a kredit je stále rezervovaný. Ak MLS správa dorazí skôr, než stihneme túto udalosť odoslať, spravidla sa neodošle a príde rovno invoice.accepted alebo invoice.rejected. Pri príjemcovi mimo siete Peppol: finančná správa prijala daňové hlásenie, status: "DELIVERED_NON_PEPPOL" — tu je to konečný stav a kredit sa odpočíta. |
invoice.accepted | Prístupový bod odberateľa poslal kladnú MLS správu — faktúra je doručená: status: "ACCEPTED", mls_c3_status: "OK". Vtedy sa odpočíta rezervovaný kredit. Neznamená to, že odberateľ faktúru obchodne schválil. |
invoice.unconfirmed | MLS správa nedorazila do 25 minút od odoslania; faktúra prešla do UNCONFIRMED a kredit sa odpočítal. Ak MLS správa dorazí neskôr, príde ešte invoice.accepted alebo invoice.rejected. Payload nesie len invoice_id a reason. |
invoice.rejected | Prístupový bod odberateľa faktúru odmietol zápornou MLS správou — alebo pri príjemcovi mimo Peppolu odmietla daňové hlásenie finančná správa. |
mls.received | Prišla odpoveď k daňovému hláseniu, kým sa na potvrdenie od protistrany ešte čaká. Stav faktúry sa nemení. |
Poradie udalostí nie je zaručené. invoice.delivered môže výnimočne prísť až po
invoice.accepted alebo invoice.rejected k tej istej faktúre. O stave faktúry
rozhodujte podľa poľa status v data, nie podľa názvu ani poradia udalostí: DELIVERED
nikdy neprepisuje konečný stav, ktorý ste už prijali, a ten istý názov invoice.delivered
nesie pri Peppole DELIVERED, mimo Peppolu DELIVERED_NON_PEPPOL.