Přeskočit na hlavní obsah

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:

AkcePopisek v aplikaciKdy se volá
createdVytvoření dokladuvznikl nový doklad
editedEditace dokladudoklad se změnil
deletedSmazání dokladudoklad byl smazán
paidÚhrada dokladuk 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á.

Tělu požadavku se nesmí věřit

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_time PHP. 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:

  • timestamp tady 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ý dokladKlíč platby v těle
invoice_issued, proforma_issued, creditnote_receivedpayment_received
invoice_received, proforma_received, creditnote_issuedpayment_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 webhookuZdroj APICesta
invoice_issuedFaktury vydané/invoice_issued/{id}
invoice_receivedFaktury přijaté/invoice_received/{id}
proforma_issuedZálohové faktury vydané/proforma_invoice_issued/{id}
proforma_receivedZálohové faktury přijaté/proforma_invoice_received/{id}
creditnote_issuedOpravné daňové doklady vydané/creditnote_issued/{id}
creditnote_receivedOpravné daňové doklady přijaté/creditnote_received/{id}
order_issuedObjednávky vydané/order_issued/{id}
order_receivedObjednávky/order_received/{id}
payment_issuedPlatby vydané/payment_issued/{id}
payment_receivedPlatby přijaté/payment_received/{id}
direct_expenseInterní předpisy nákladů/direct_expense/{id}
Zálohové faktury mají v webhooku jiný název

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