Webhooky
Otto pošle na vaši adresu POST, když se něco stane: nová konverzace, předání, kontakt, reklamace, načtený katalog. Podpis podle Standard Webhooks.
Nastavení
- V panelu otevřete Nastavení, část Propojení, řádek Webhooky.
- Zadejte veřejnou adresu
https://vašeho serveru a vyberte události. Když necháte označené všechny, pošleme i události, které přibudou později. - Uložte. Panel ukáže podpisový klíč
whsec_…, uložte si ho k ověření podpisu. - Tlačítkem Poslat zkušební událost ověříte spojení hned. Přijde událost
test.ping.

Události
| Událost | Kdy přijde | data |
|---|---|---|
conversation.started | Začala nová konverzace Když zákazník napíše první zprávu. | { conversationId, question } |
conversation.handoff | Zákazník chce člověka Otto neví dál nebo zákazník výslovně žádá člověka. | { conversationId, question, confidence } |
conversation.operator_joined | Rozhovor převzal člověk Někdo z týmu převzal rozhovor ve Schránce. | { conversationId, operator } |
conversation.resolved | Konverzace je vyřešená Označili jste ji ve Schránce jako vyřešenou. | { conversationId } |
conversation.rated | Zákazník ohodnotil konverzaci Palec nahoru nebo dolů na konci konverzace. | { conversationId, rating } |
message.created jen na výběr | Nová zpráva v konverzaci Každá zpráva zákazníka, asistenta i kolegy. Je jich hodně, zapněte ji, jen když je opravdu zpracujete. | { id, conversationId, role, kind, text, author } |
lead.created | Zákazník zanechal kontakt Jméno, e-mail nebo telefon zadaný přímo v chatu. | { id, conversationId, name, email, phone } |
lead.updated | Kontakt je vyřízený nebo znovu otevřený Někdo v panelu nebo přes API změnil, zda je kontakt vyřízený. | { id, conversationId, handled } |
return.created | Nová reklamace nebo vrácení Zákazník odeslal formulář i s přílohami. | { id, kind, orderId } |
return.updated | Změnil se stav podnětu Schválili jste ho, zamítli nebo vyřídili. | { id, kind, orderId, status } |
unanswered.created | Nejistá odpověď Otázka, na kterou se ve vašich znalostech nenašla jistá odpověď. | { conversationId, question } |
catalog.synced | Katalog se načetl Feed produktů se načetl, přijde i počet produktů. | { source, products } |
catalog.failed | Katalog se nenačetl Feed produktů se nepodařilo načíst. Přijde při prvním selhání, ne při každém dalším. | { source, error } |
usage.threshold | Dochází limit AI odpovědí Přijde při 80 % a při 100 % limitu AI odpovědí v plánu. | { used, limit, level } |
Událost message.created chodí jen tehdy, když ji zapnete sami. Je jich hodně a tlačítko Označit všechny ji vynechá.
Tělo požadavku
{
"id": "1b6f6a3e-0f7c-4a51-9a3d-6a2f0e8c4b11",
"event": "lead.created",
"at": "2026-10-10T08:14:03.512Z",
"apiVersion": "1",
"tenantId": "3f6c2a10-8d4e-4c1b-a6f2-9e0d7b5c3a21",
"tenant": "Váš obchod",
"data": {
"id": "8353028d-e582-4e08-8ed9-f882d200cd22",
"conversationId": "37e7d86d-57bf-455f-9fc6-8c856a607d2f",
"name": "Ján Novák",
"email": "jan@example.sk",
"phone": ""
}
}id je při opakování stejné. Když totéž id dorazí znovu, záznam zahoďte.
Ověření podpisu
Podepisujeme podle standardu Standard Webhooks, ověření zvládne i hotová knihovna pro Node.js, PHP, Python a další jazyky. Každý požadavek nese tři hlavičky:
webhook-id | id události, při opakování stejné |
webhook-timestamp | čas pokusu v sekundách od 1. 1. 1970 |
webhook-signature | v1,<podpis>, kde podpis je Base64 z HMAC-SHA256 nad id.timestamp.tělo |
Klíčem HMAC je podpisový klíč bez předpony whsec_, dekódovaný z Base64. Podpis počítejte ze surového těla, ještě před parsováním JSON. Pokus odmítněte, když podpis nesedí nebo je čas starší než 5 minut.
import crypto from "node:crypto";
export function overPodpis(secret, hlavicky, suroveTelo) {
const id = hlavicky["webhook-id"];
const cas = hlavicky["webhook-timestamp"];
const kluc = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const ocakavany = crypto.createHmac("sha256", kluc).update(id + "." + cas + "." + suroveTelo).digest("base64");
const sedi = String(hlavicky["webhook-signature"] || "").split(" ").some((cast) => {
const [verzia, podpis] = cast.split(",");
return verzia === "v1" && podpis && podpis.length === ocakavany.length
&& crypto.timingSafeEqual(Buffer.from(podpis), Buffer.from(ocakavany));
});
return sedi && Math.abs(Date.now() / 1000 - Number(cas)) < 300;
}function overPodpis(string $secret, array $h, string $suroveTelo): bool {
$kluc = base64_decode(preg_replace('/^whsec_/', '', $secret));
$sprava = $h['webhook-id'] . '.' . $h['webhook-timestamp'] . '.' . $suroveTelo;
$ocakavany = base64_encode(hash_hmac('sha256', $sprava, $kluc, true));
foreach (explode(' ', $h['webhook-signature'] ?? '') as $cast) {
[$verzia, $podpis] = array_pad(explode(',', $cast, 2), 2, '');
if ($verzia === 'v1' && hash_equals($ocakavany, $podpis)) {
return abs(time() - (int) $h['webhook-timestamp']) < 300;
}
}
return false;
}import base64, hashlib, hmac, time
def over_podpis(secret: str, h: dict, surove_telo: bytes) -> bool:
kluc = base64.b64decode(secret.removeprefix("whsec_"))
sprava = f"{h['webhook-id']}.{h['webhook-timestamp']}.".encode() + surove_telo
ocakavany = base64.b64encode(hmac.new(kluc, sprava, hashlib.sha256).digest()).decode()
sedi = any(
hmac.compare_digest(cast.split(",", 1)[1], ocakavany)
for cast in h.get("webhook-signature", "").split()
if cast.startswith("v1,")
)
return sedi and abs(time.time() - int(h["webhook-timestamp"])) < 300Původní hlavičku X-Otto-Signature: sha256=<HMAC_SHA256(secret, tělo) v hex> posíláme dál, starší integrace fungují beze změny.
Opakování a vypnutí
Odpovězte kódem 2xx do 5 sekund, práci dělejte až potom. Když odpověď nepřijde nebo není 2xx, zkusíme to znovu v těchto odstupech: hned, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h, 14 h, 24 h, celkem asi 3 dny. Události čekají v databázi, nepropadnou ani při našem restartu. U kódu 4xx kromě 408 a 429 neopakujeme.
Když 3 dny nedoručíme ani jednu událost, webhook vypneme a pošleme vám e-mail. Zapnete ho tím, že ho v panelu znovu uložíte.
Doručení přes API
GET /api/v1/webhooks/deliveries?status=failedvypíše doručení s počtem pokusů, kódem odpovědi a chybou.POST /api/v1/webhooks/deliveries/<id>/resendpošle událost znovu. Obsah události držíme 7 dní po posledním pokusu.POST /api/v1/webhooks/testpošle zkušební událost a hned řekne, jak vaše adresa odpověděla.

