Webhooks avisam outros sistemas quando algo muda no KrakenDock: um card foi criado ou movido, alguém comentou, uma sprint foi encerrada, uma página da wiki foi editada. A cada evento, o KrakenDock faz um POST para a URL que você cadastrou.
Use webhooks para disparar o CI, atualizar um painel interno, sincronizar com outra ferramenta ou postar avisos num canal do Slack ou do Discord. Este guia é para quem administra um projeto ou uma organização e para quem escreve o sistema que recebe os eventos.
Versão em Markdown puro deste guia: https://app.krakendock.com/docs/webhooks.md?lang=pt-BR. Use-a para colar num assistente de IA ou na documentação interna do seu time.
Um webhook pertence a um projeto ou a uma organização:
Há três tipos. O tipo não muda depois de criado.
| Tipo | O que é enviado | Assinatura |
|---|---|---|
| Webhook genérico | o evento em JSON (veja Formato do payload) | sim, HMAC SHA-256 com um segredo whsec_… |
| Slack | uma mensagem pronta para um canal, por um Incoming Webhook do Slack | não tem segredo |
| Discord | uma mensagem pronta para um canal, por um webhook do Discord | não tem segredo |
Você precisa da permissão Gerenciar projeto (veja Permissões).
https e ser um endereço público. Veja Restrições de URL e segurança.O webhook nasce Ativo. Para pausar, use Desativar no menu de ações; para voltar, Reativar. Nesse menu também ficam Editar, Ver entregas, Gerar novo segredo (só no genérico) e Excluir.
Ao editar, você pode trocar o nome, a URL, os eventos, o idioma e os projetos. Deixe a URL em branco para manter a atual. Depois de salva, a URL nunca aparece inteira: a tela mostra só o começo e os últimos caracteres.
Para receber os eventos de vários projetos com um webhook só, vá em Configurações da organização → Integrações. A seção de webhooks fica abaixo do GitHub. Você precisa da permissão Gerenciar integrações da organização.
O formulário tem o campo Projetos:
Os eventos são os mesmos do webhook de projeto, vindos dos projetos cobertos. Cada payload traz o project de origem e a organization. Só o ping do teste chega sem project.
Se alguém criou um webhook que cobre projetos que você não acessa, ele aparece na lista com o selo Sem acesso. Você não vê os detalhes e não pode alterá-lo.
| Evento | Rótulo na tela | Quando dispara |
|---|---|---|
card.created | Card criado | um card é criado |
card.updated | Card atualizado | muda título, descrição, tipo, prioridade, estimativa, prazo, responsável, sprint, labels, card pai ou campo customizado; o card é arquivado, desarquivado ou reaberto |
card.moved | Card movido | o card muda de coluna ou de board |
card.completed | Card finalizado | o card é finalizado (inclusive ao finalizar uma coluna inteira ou ao criar um card já finalizado) |
card.deleted | Card excluído | o card é excluído |
comment.created | Comentário criado | alguém comenta num card |
sprint.started | Sprint iniciada | uma sprint é iniciada |
sprint.closed | Sprint encerrada | uma sprint é encerrada |
wiki.page.updated | Página da wiki atualizada | uma página da wiki é editada, movida ou renomeada |
import.completed | Importação de issues concluída | uma importação de issues do GitHub termina, com sucesso ou com falha |
O teste envia um evento ping, que não aparece na lista porque não dá para escolhê-lo entre os eventos do webhook.
card.updated por card e por operação, com todas as mudanças em changes. Se um campo volta ao valor original na mesma operação, a mudança some.card.moved. card.completed sai quando alguém usa Finalizar. Se as duas coisas acontecem juntas, saem os dois eventos.sprint.closed, com completed_cards e moved_cards. Os cards levados para o backlog ou para a próxima sprint não geram card.updated.import.completed. Os cards importados não geram card.created.O webhook genérico envia application/json. Os campos fazem parte de um contrato público: podem ganhar campos novos, mas nunca são renomeados nem removidos. Ignore campos que você não conhece.
| Campo | Descrição |
|---|---|
id | UUID do evento. Novas tentativas e reenvios mantêm o mesmo id |
type | o evento (card.moved, comment.created, …; ping no teste) |
occurred_at | quando aconteceu, em UTC (RFC 3339) |
project | {id, key, name, handle, url}. handle é o usuário dono ou o slug da organização. Não vem no ping |
organization | {id, slug, name}, quando o projeto é de uma organização |
actor | {id, name, username} de quem fez a mudança. Não vem em mudanças do sistema |
data | o objeto do evento: card, comment (com o card), sprint, wiki_page, import ou, no ping, webhook |
changes | lista de {field, from, to}, em card.updated e card.moved |
data.card é o card depois da mudança: id, key, title, type (task, bug, story), priority (lowest a highest), column {id, name}, board {id, name}, sprint {id, name}, assignee {id, name, username}, labels [{id, name, color}], parent {id, key, title}, estimate, due_date (AAAA-MM-DD), completed_at, archived_at, deleted_at, url e custom_fields. Campos opcionais vazios não vêm. labels e custom_fields são sempre listas.
A descrição do card nunca vai no payload. Em changes, o item description só avisa que ela mudou.
Os outros objetos:
data.comment: id, excerpt (até 500 caracteres do texto), truncated (se foi cortado) e url (o link do card).data.sprint: id, name, goal, board, status, starts_at, ends_at, url e, em sprint.closed, completed_cards e moved_cards.data.wiki_page: id, title, version e url.data.import: id, status (done ou failed), source (github), repository {full_name}, board, total_count, created_count, skipped_count, failed_count, started_at, finished_at, error_message (só em failed) e url.data.webhook (no ping): id e name do webhook.{
"id": "0199e4a0-1b2c-7d3e-8f40-5a6b7c8d9e01",
"type": "card.created",
"occurred_at": "2026-10-08T13:58:02Z",
"project": {
"id": "0199d0aa-1111-7000-8000-000000000001",
"key": "KRK",
"name": "Kraken",
"handle": "acme",
"url": "https://app.krakendock.com/acme/KRK"
},
"organization": { "id": "0199d0aa-2222-7000-8000-000000000002", "slug": "acme", "name": "Acme" },
"actor": {
"id": "0199d0aa-3333-7000-8000-000000000003",
"name": "Maria Silva",
"username": "maria"
},
"data": {
"card": {
"id": "0199e49b-aaaa-7000-8000-00000000000a",
"key": "KRK-42",
"title": "Corrigir login",
"type": "bug",
"priority": "high",
"column": { "id": "0199d0ab-0000-7000-8000-0000000000c1", "name": "To Do" },
"board": { "id": "0199d0ab-0000-7000-8000-0000000000b1", "name": "Backend" },
"sprint": { "id": "0199d0ab-0000-7000-8000-0000000000e1", "name": "Sprint 12" },
"assignee": {
"id": "0199d0aa-3333-7000-8000-000000000003",
"name": "Maria Silva",
"username": "maria"
},
"labels": [
{ "id": "0199d0ab-0000-7000-8000-0000000000d1", "name": "auth", "color": "#3b82f6" }
],
"estimate": 3,
"due_date": "2026-10-15",
"url": "https://app.krakendock.com/acme/KRK/cards/KRK-42",
"custom_fields": [
{
"id": "0199d0ab-0000-7000-8000-0000000000f1",
"name": "Cliente",
"type": "text",
"value": "Acme Ltda"
}
]
}
}
}
{
"id": "0199e4a3-5d6e-7f80-9a1b-2c3d4e5f6a70",
"type": "comment.created",
"occurred_at": "2026-10-08T14:05:40Z",
"project": {
"id": "0199d0aa-1111-7000-8000-000000000001",
"key": "KRK",
"name": "Kraken",
"handle": "acme",
"url": "https://app.krakendock.com/acme/KRK"
},
"organization": { "id": "0199d0aa-2222-7000-8000-000000000002", "slug": "acme", "name": "Acme" },
"actor": {
"id": "0199d0aa-4444-7000-8000-000000000004",
"name": "João Souza",
"username": "joao"
},
"data": {
"card": {
"id": "0199e49b-aaaa-7000-8000-00000000000a",
"key": "KRK-42",
"title": "Corrigir login",
"type": "bug",
"priority": "high",
"column": { "id": "0199d0ab-0000-7000-8000-0000000000c2", "name": "In Progress" },
"board": { "id": "0199d0ab-0000-7000-8000-0000000000b1", "name": "Backend" },
"labels": [],
"url": "https://app.krakendock.com/acme/KRK/cards/KRK-42",
"custom_fields": []
},
"comment": {
"id": "0199e4a3-0000-7000-8000-0000000000aa",
"excerpt": "Reproduzi no Safari. O cookie de sessão não volta depois do redirect.",
"truncated": false,
"url": "https://app.krakendock.com/acme/KRK/cards/KRK-42"
}
}
}
Cada item é {field, from, to}, com null quando o valor está vazio:
field | from / to |
|---|---|
title, type, priority | texto |
estimate | número ou null |
due_date | "AAAA-MM-DD" ou null |
assignee | {id, name, username} ou null |
sprint | {id, name} ou null |
labels | lista [{id, name, color}] |
parent | {id, key, title} ou null |
description | sem from e to: só avisa que mudou |
completed | true → false quando o card é reaberto |
archived | false → true ao arquivar, e o contrário ao desarquivar |
custom_field | {field: {id, name}, type, value, options} nos dois lados; um item por campo alterado |
column (em card.moved) | {column: {id, name}, board: {id, name}} |
Toda entrega do webhook genérico leva:
| Cabeçalho | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Kraken-Webhooks/1.0 |
X-Kraken-Event | o evento (card.created, ping, …) |
X-Kraken-Delivery | o id da entrega. Igual em todas as tentativas; um reenvio manual cria uma entrega nova, com outro id |
X-Kraken-Timestamp | hora do envio em segundos Unix, gerada a cada tentativa |
X-Kraken-Signature-256 | sha256= seguido do HMAC em hexadecimal |
Confira a assinatura antes de confiar numa entrega:
<X-Kraken-Timestamp>.<corpo bruto>: o valor do cabeçalho, um ponto e o corpo.whsec_.sha256=<hex> com o cabeçalho X-Kraken-Signature-256 numa comparação de tempo constante.import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.KRAKEN_WEBHOOK_SECRET; // whsec_...
const app = express();
app.post('/hooks/kraken', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Kraken-Timestamp') ?? '';
const signature = req.get('X-Kraken-Signature-256') ?? '';
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !Number.isFinite(age) || age > 300) return res.sendStatus(400);
const expected =
'sha256=' +
crypto.createHmac('sha256', SECRET).update(`${timestamp}.`).update(req.body).digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// Enfileire o evento e ignore um event.id que você já processou.
res.sendStatus(204);
});
app.listen(3000);
import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
SECRET = os.environ["KRAKEN_WEBHOOK_SECRET"].encode() # whsec_...
app = Flask(__name__)
def verify(headers, raw_body: bytes) -> bool:
timestamp = headers.get("X-Kraken-Timestamp", "")
signature = headers.get("X-Kraken-Signature-256", "")
try:
if abs(time.time() - int(timestamp)) > 300:
return False
except ValueError:
return False
expected = "sha256=" + hmac.new(
SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.post("/hooks/kraken")
def kraken_webhook():
if not verify(request.headers, request.get_data()):
abort(401)
event = request.get_json()
# Enfileire o evento e ignore um event["id"] que você já processou.
return "", 204
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strconv"
"time"
)
var secret = []byte(os.Getenv("KRAKEN_WEBHOOK_SECRET")) // whsec_...
func verify(r *http.Request, body []byte) bool {
ts := r.Header.Get("X-Kraken-Timestamp")
sec, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return false
}
if age := time.Since(time.Unix(sec, 0)); age > 5*time.Minute || age < -5*time.Minute {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(r.Header.Get("X-Kraken-Signature-256")), []byte(expected))
}
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil || !verify(r, body) {
w.WriteHeader(http.StatusUnauthorized)
return
}
// Enfileire o evento e ignore um "id" que você já processou.
w.WriteHeader(http.StatusNoContent)
}
func main() {
http.HandleFunc("/hooks/kraken", handler)
_ = http.ListenAndServe(":8080", nil)
}
Se o segredo vazar, use Gerar novo segredo. O segredo antigo deixa de valer na hora, então atualize o seu sistema logo em seguida.
occurred_at para ordenar.Depois de 5 entregas seguidas que esgotaram todas as tentativas, o KrakenDock desativa o webhook. Ele aparece como Desativado por falhas, e quem criou o webhook recebe a notificação Webhook desativado (no app e por e-mail, conforme as preferências de notificação). Uma entrega bem-sucedida zera o contador.
Ao desativar um webhook, à mão ou por falhas:
Para voltar, corrija o destino, use Enviar teste (funciona com o webhook desativado) e clique em Reativar. Reativar zera o contador de falhas, mas não reenvia o que foi descartado.
Enviar teste manda na hora um evento ping, com data.webhook (id e name) e você como actor. O resultado aparece em seguida: Teste enviado. ou O teste falhou. O teste tem uma só tentativa e nunca conta para a desativação automática.
{
"id": "0199e4b0-0000-7000-8000-000000000099",
"type": "ping",
"occurred_at": "2026-10-08T15:00:00Z",
"actor": {
"id": "0199d0aa-3333-7000-8000-000000000003",
"name": "Maria Silva",
"username": "maria"
},
"data": {
"webhook": { "id": "0199e4af-0000-7000-8000-000000000010", "name": "Notificador do CI" }
}
}
Clique em Ver entregas para abrir o histórico, do mais recente para o mais antigo. Cada entrega fica guardada por 30 dias e mostra:
Abra uma entrega para ver o Erro, a Resposta do destino (até 2 KB), o Conteúdo enviado (com o botão Copiar) e as datas.
Reenviar cria uma entrega nova com o mesmo evento e o mesmo corpo. O id do evento continua o mesmo; o X-Kraken-Delivery muda. Só dá para reenviar uma entrega Entregue ou Falhou, e com o webhook ativo. O reenvio de um teste sai na hora, como Enviar teste.
https://hooks.slack.com/services/….https://discord.com/api/webhooks/…. Também valem discordapp.com, ptb.discord.com e canary.discord.com.@everyone no texto dos cards não notificam ninguém.https. URLs com usuário e senha (https://user:pass@…) ou com #fragmento são recusadas. A URL tem até 2.048 caracteres.localhost, 127.0.0.1), redes privadas (10.x, 172.16–31.x, 192.168.x), link-local, CGNAT (100.64.0.0/10), faixas reservadas e endereços de metadados de nuvem (169.254.169.254). O KrakenDock confere o endereço no cadastro e de novo a cada conexão, então trocar o DNS depois não adianta.https público (por exemplo, ngrok ou Cloudflare Tunnel).| O que | Permissão |
|---|---|
| Webhooks de projeto: ver, criar, editar, testar, ver entregas, reenviar, excluir | Gerenciar projeto (o dono de um projeto pessoal tem sempre) |
| Webhooks de organização: as mesmas ações | Gerenciar integrações da organização (o Owner tem por padrão) |
| Webhook de organização em Todos os projetos (inclui novos) | também Gerenciar projetos da organização |
| Webhook de organização em Projetos selecionados | também Ver projeto em cada projeto escolhido |
A lista da organização mostra todos os webhooks dela. Quem não alcança os projetos de um webhook vê o selo Sem acesso e não vê a URL nem os projetos.
Os webhooks estão disponíveis em todos os planos, inclusive o Free.
| Limite | Valor |
|---|---|
| Webhooks por projeto ou por organização | 10 |
| Projetos selecionados num webhook de organização | 100 |
| Enviar teste | 20 por minuto, por pessoa |
| Reenviar | 60 por minuto, por pessoa |
| Histórico de entregas | 30 dias |
Trecho do comentário em comment.created | 500 caracteres |
id do evento, o campo id do corpo. Ele se repete em novas tentativas e em reenvios manuais. O X-Kraken-Delivery muda num reenvio manual, então não serve sozinho para deduplicar.occurred_at ou busque o estado atual do card quando a ordem importar.Não dá: o segredo aparece uma única vez. Use Gerar novo segredo e atualize o seu sistema. O antigo deixa de valer na hora.
Porque mover de coluna gera só card.moved. O card.completed sai quando alguém finaliza o card (Finalizar no card ou ao finalizar a coluna inteira).
Não. A entrega é "pelo menos uma vez". Ignore um id de evento que você já processou.
Confira se o webhook está Ativo, se os Eventos certos estão marcados e se a mudança gera evento (veja Regras dos eventos). Em Ver entregas, olhe o status, a resposta HTTP e o erro de cada entrega.
Não. A descrição nunca vai no payload. Use o url do card ou o servidor MCP para ler o card completo.
Não. O destino precisa ser https e público. Para testar localmente, use um túnel https.
import.completed.custom_fields.