Webhooky
Webhook je odchozí HTTP volání: iÚčto pošle POST na vaši URL, když se
v účetnictví něco stane. Není to endpoint API — vaše aplikace nic nevolá, jen
naslouchá.
Tvar těla je popsaný v referenci u Změny dokladu a Úhrady dokladu. Tahle stránka popisuje provozní stránku věci: kde se webhooky nastavují, co zaručují a co ne.
Kde se webhooky nastavují
Webhooky se nastavují jen v aplikaci, na stránce Nastavení → Automatizace → Nastavení API → Webhooky. Přes API se vytvořit, změnit ani vypsat nedají — žádný takový zdroj neexistuje.
Nastavení je tabulka řádků. Každý řádek má jednu URL a výběr akcí, které se na ni mají posílat:
| Akce | Popisek v aplikaci | Kdy se volá |
|---|---|---|
created | Vytvoření dokladu | vznikl nový doklad |
edited | Editace dokladu | doklad se změnil |
deleted | Smazání dokladu | doklad byl smazán |
paid | Úhrada dokladu | k dokladu se navázala platba |
Jiné akce neexistují. Jedna URL může přijímat všechny čtyři akce, nebo si
můžete pro každou akci založit vlastní řádek s vlastní URL — hodnota action
v těle je v obou případech vyplněná, takže se příjemce může rozhodnout podle ní.
Dvě věci, které nejsou na první pohled vidět:
- Webhook se neomezuje na změny provedené přes API. Volání se spouští při uložení dokladu, takže se ozve i tehdy, když doklad založí uživatel ručně ve webovém rozhraní nebo automatický přenos z e-shopu.
- Nastavení patří dvojici uživatel + firma, ale při události se obesílají webhooky všech uživatelů dané firmy. Když si dva kolegové nastaví každý svou URL, dostanou oznámení oba.
Žádný podpis, žádná autentizace
iÚčto k webhooku nepřikládá žádný podpis ani HMAC. Do těla ani do hlaviček se nedává API klíč, sdílené tajemství, ani nic jiného, čím by šlo ověřit odesílatele. Jediná ochrana je ta, že vaši URL nikdo jiný nezná.
Kdokoli, kdo vaši URL uhodne nebo odposlechne, může poslat stejný POST.
Přijímací endpoint proto nesmí data z těla zapisovat. Berte z těla jen ID
a typ dokladu a skutečný stav si dotáhněte GET požadavkem na API s vaším
klíčem. Údaje v těle úhrady (částky, zbytek k úhradě) jsou informativní.
Co s tím jde udělat:
- Dejte do URL dlouhý náhodný segment (
https://…/hooks/iucto/8f3c…) a berte ho jako tajemství — tedy ne do repozitáře, ne do logů. - Použijte HTTPS. Aplikace formát URL kontroluje, ale schéma nevynucuje;
přes
http://by tělo šlo po síti v otevřené podobě. - Certifikát cílového serveru se ověřuje proti vestavěné CA sadě. Endpoint se samopodepsaným certifikátem oznámení nedostane.
Doručení: jeden pokus, žádný retry
Když volání selže, iÚčto ho neopakuje. Chyba (nedostupný server, timeout, chybný certifikát, návratový kód 4xx/5xx) se jen zaloguje na straně iÚčta a událost je nenávratně pryč. Fronta ani odložené doručení neexistují.
Další vlastnosti, se kterými je potřeba počítat:
- Odpověď se nezpracovává. Stačí jakýkoli kód 2xx, tělo odpovědi nikdo nečte.
- Volání je synchronní a nemá timeout. Odesílá se v průběhu ukládání
dokladu a iÚčto čeká, dokud příjemce neodpoví — pomalý endpoint tedy drží
ukládání dokladu, dokud nespadne na
max_execution_timePHP. Rychlá odpověď proto není doporučení, ale podmínka: přijměte tělo, zařaďte do fronty a odpovězte. - Pořadí není zaručené. Rychlá dvojice vytvoření + editace může dorazit přeházená.
Z jednoho pokusu bez opakování plyne, že se na webhook nedá stavět jako na jediném zdroji dat. Berte ho jako pobídku „podívej se na doklad X" a stav si načtěte přes API. Kdo potřebuje jistotu, ať k tomu přidá pravidelnou kontrolu — třeba noční průchod dokladů změněných za poslední den.
Každý takový GET se počítá do denní kvóty dotazů. Při návrhu integrace proto
počítejte s tím, že jedna událost znamená minimálně jeden dotaz navíc — kolik
dotazů má který tarif a jak je šetřit, je v Limitech a kvótách.
Co je v těle
Tělo je vždy JSON. Vypadá jinak pro změnu dokladu a jinak pro úhradu.
Změna dokladu (created, edited, deleted)
Tělo nese action, timestamp a jeden klíč pojmenovaný podle typu
dokladu, ve kterém je jen id. Žádná další data o dokladu se neposílají.
{
"action": "created",
"invoice_issued": {
"id": 4567
},
"timestamp": "2020-09-23T11:23:45+0200"
}
timestamp je ISO 8601 s posunem bez dvojtečky (+0200). To není platný
date-time podle RFC 3339, takže striktní parsery ho odmítnou — v Javascriptu
je potřeba ho před new Date() upravit.
Úhrada dokladu (paid)
Tělo nese action, uhrazený doklad s částkami a platbu, která ho uhradila.
{
"action": "paid",
"invoice_issued": {
"id": 123,
"currency": "EUR",
"price_inc_vat": 100,
"price_inc_vat_czk": 2650,
"to_be_paid": 24.5
},
"payment_received": {
"id": 123,
"currency": "CZK",
"price_inc_vat": 2000,
"price_inc_vat_czk": 2000
}
}
Dva rozdíly proti změně dokladu:
timestamptady není. Payload úhrady ho neobsahuje. Kdo si čas události zaznamenává, musí použít čas přijetí požadavku.- Volání přijde i při částečné úhradě. Jestli je doklad doplacený, poznáte
podle
to_be_paid— je to zbytek k úhradě po této platbě.
Úhrady se posílají jen u šesti typů dokladů:
| Uhrazený doklad | Klíč platby v těle |
|---|---|
invoice_issued, proforma_issued, creditnote_received | payment_received |
invoice_received, proforma_received, creditnote_issued | payment_issued |
Klíče typů dokladů
Klíč v těle říká, o jaký doklad jde, a určuje, na který zdroj API se ptát. Většina klíčů odpovídá cestě zdroje, dva ne.
| Klíč v těle webhooku | Zdroj API | Cesta |
|---|---|---|
invoice_issued | Faktury vydané | /invoice_issued/{id} |
invoice_received | Faktury přijaté | /invoice_received/{id} |
proforma_issued | Zálohové faktury vydané | /proforma_invoice_issued/{id} |
proforma_received | Zálohové faktury přijaté | /proforma_invoice_received/{id} |
creditnote_issued | Opravné daňové doklady vydané | /creditnote_issued/{id} |
creditnote_received | Opravné daňové doklady přijaté | /creditnote_received/{id} |
order_issued | Objednávky vydané | /order_issued/{id} |
order_received | Objednávky | /order_received/{id} |
payment_issued | Platby vydané | /payment_issued/{id} |
payment_received | Platby přijaté | /payment_received/{id} |
direct_expense | Interní předpisy nákladů | /direct_expense/{id} |
Webhook používá klíče proforma_issued a proforma_received, ale zdroje API
se jmenují proforma_invoice_issued a proforma_invoice_received. Klíč z těla
se tedy nedá použít přímo jako cesta — je potřeba mapovací tabulka. Kdo
sestavuje URL zřetězením, dostane u zálohových faktur 404.
Klíč direct_expense chodí jen u akcí created, edited a deleted;
u úhrady se nepoužívá.
Příklad příjemce
Endpoint níže dělá tři věci: ověří tajemství v cestě, z těla vezme jen ID a typ,
a stav si dotáhne z API. Zápis do vlastní databáze je až za tím GET.
<?php
// POST https://example.com/hooks/iucto/<TAJEMSTVI>
if (!hash_equals(getenv('IUCTO_HOOK_SECRET'), $_GET['secret'] ?? '')) {
http_response_code(404);
exit;
}
// Klíč v těle odpovídá cestě zdroje, kromě dvou výjimek z tabulky výš.
$vyjimky = [
'proforma_issued' => 'proforma_invoice_issued',
'proforma_received' => 'proforma_invoice_received',
];
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$action = $body['action'] ?? null;
foreach ($body as $key => $value) {
if (!is_array($value) || !isset($value['id'])) {
continue;
}
// Odpovězte hned, práci si zařaďte do fronty — volání je synchronní
// a iÚčto ho při chybě neopakuje.
fronta_zarad($action, $vyjimky[$key] ?? $key, (int) $value['id']);
}
http_response_code(200);
Fronta pak stav dotáhne z API — GET /{resource}/{id} s hlavičkou
X-Auth-Key, viz Autentizace. Čte se z API, ne z těla
webhooku.
U akce deleted se na dotažení stavu nespoléhejte. Smazání dokladu má dvě
podoby: doklad se z databáze buď odstraní úplně (a GET pak vrátí 404), nebo
se jen označí jako smazaný (a GET vrátí 200 s deleted: true). Co se
stane, závisí na pozici dokladu v číselné řadě, viz
Smazání a stav deleted. Zpracujte proto tuhle akci
podle ID z těla a s oběma odpověďmi počítejte.
Kam dál
- Změna dokladu — schéma těla v referenci
- Úhrada dokladu — schéma těla v referenci
- Autentizace — klíč pro dotažení stavu přes API
- Limity a kvóty — kolik dotazů stojí dotažení stavu po každé události
- Chyby a jak je číst — stavové kódy a tvary chybových odpovědí