# Webhooks

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](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.

## Visão geral

Um webhook pertence a **um projeto** ou a **uma organização**:

- **Webhook de projeto**: recebe os eventos desse projeto.
- **Webhook de organização**: recebe os eventos de todos os projetos da organização (inclusive os criados depois) ou só dos projetos que você escolher. Projetos pessoais não entram em webhook de 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](#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                            |

## Criar um webhook

Você precisa da permissão **Gerenciar projeto** (veja [Permissões](#permissoes)).

1. Abra o projeto e vá em **Configurações → Webhooks**. A opção fica no grupo **Integrações**.
2. Clique em **Novo webhook**.
3. Escolha o **Tipo**: **Webhook genérico**, **Slack** ou **Discord**.
4. Dê um **Nome** (até 100 caracteres, só para você identificar o webhook).
5. Cole a **URL**. Ela precisa usar `https` e ser um endereço público. Veja [Restrições de URL e segurança](#restricoes-de-url-e-seguranca).
6. Em **Eventos**, marque os eventos que disparam o webhook (pelo menos um) ou use **Selecionar todos os eventos**.
7. No Slack e no Discord, escolha o **Idioma das mensagens**: Português (Brasil) ou English. O padrão é o seu idioma.
8. Clique em **Criar webhook**.
9. No webhook genérico, aparece a janela **Segredo do webhook**. Copie o segredo e guarde num lugar seguro: ele é mostrado **uma única vez**. Depois clique em **Já guardei o segredo**.
10. No menu de ações do webhook, clique em **Enviar teste** para conferir se o seu sistema recebe o evento.

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.

### Webhook de organização

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**:

- **Todos os projetos (inclui novos)**: exige também a permissão **Gerenciar projetos** da organização.
- **Projetos selecionados**: até 100 projetos. Você precisa ter acesso a cada um.

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.

## Eventos

| 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.

### Regras dos eventos

- **Um `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.
- **Mover não é finalizar.** Mover o card para a coluna "Done" gera só `card.moved`. `card.completed` sai quando alguém usa **Finalizar**. Se as duas coisas acontecem juntas, saem os dois eventos.
- **Operações em massa** (como finalizar uma coluna inteira): o webhook genérico recebe um evento por card. Slack e Discord recebem uma mensagem-resumo, com a contagem e os 10 primeiros cards.
- **Encerrar uma sprint** gera só `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`.
- **Importação de issues** gera um único `import.completed`. Os cards importados não geram `card.created`.
- Reordenar cards dentro da coluna, restaurar card, travar comentários e anexos não geram evento.
- Criar ou excluir uma página da wiki não gera evento.

## Formato do payload

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.

### Envelope

| 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.

### Exemplo: card.created

```json
{
  "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"
        }
      ]
    }
  }
}
```

### Exemplo: comment.created

```json
{
  "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"
    }
  }
}
```

### O campo changes

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}}`                                              |

## Cabeçalhos

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                                                             |

## Verificar a assinatura

Confira a assinatura antes de confiar numa entrega:

1. Leia o **corpo bruto** da requisição, exatamente como chegou. Não reserialize o JSON.
2. Monte a mensagem `<X-Kraken-Timestamp>.<corpo bruto>`: o valor do cabeçalho, um ponto e o corpo.
3. Calcule o HMAC SHA-256 dessa mensagem com o segredo como chave. Use o segredo inteiro, com o prefixo `whsec_`.
4. Compare `sha256=<hex>` com o cabeçalho `X-Kraken-Signature-256` numa comparação de tempo constante.
5. Recuse timestamps com mais de 5 minutos de diferença do relógio atual. Isso barra entregas repetidas por terceiros.

### Node.js

```js
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);
```

### Python

```python
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
```

### Go

```go
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.

## Entregas e novas tentativas

- **Sucesso** é qualquer resposta **2xx**. Qualquer outra resposta (inclusive 3xx), erro de rede, erro de TLS ou timeout conta como falha.
- **Timeout**: o seu sistema tem **10 segundos** para responder, contando conexão, TLS e resposta.
- **Redirects não são seguidos.** Cadastre a URL final.
- **Novas tentativas**: até **6 tentativas** por entrega. Depois da primeira, as esperas são de 1 minuto, 5 minutos, 30 minutos, 2 horas e 6 horas (cerca de 8h36 no total). Esgotadas as tentativas, a entrega fica **Falhou**.
- **Ordem**: a ordem de chegada não é garantida. Use `occurred_at` para ordenar.
- **Pelo menos uma vez**: o mesmo evento pode chegar mais de uma vez (por exemplo, quando o seu sistema processou mas respondeu depois do timeout). Veja [Boas práticas](#boas-praticas).

### Desativação automática

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:

- as entregas pendentes são descartadas;
- os eventos que acontecerem enquanto ele estiver desativado não são guardados.

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.

## Testar e reenviar

### Enviar teste

**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.

```json
{
  "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" }
  }
}
```

### Histórico de entregas

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:

- o status: **Pendente**, **Enviando**, **Entregue** ou **Falhou**;
- o número de tentativas, a resposta HTTP, a duração e a hora da próxima tentativa;
- os selos **Teste** e **Reenvio**, quando for o caso.

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**.

## Slack e Discord

- **Slack**: crie um Incoming Webhook no Slack e cole a URL `https://hooks.slack.com/services/…`.
- **Discord**: nas configurações do canal, crie um webhook e cole a URL `https://discord.com/api/webhooks/…`. Também valem `discordapp.com`, `ptb.discord.com` e `canary.discord.com`.
- Outras URLs são recusadas nesses tipos.
- Cada evento vira uma mensagem no idioma escolhido. Uma operação em massa vira uma mensagem-resumo por tipo de evento e projeto.
- No Discord, menções como `@everyone` no texto dos cards não notificam ninguém.
- Não há assinatura: quem tem a URL consegue postar no canal. Trate a URL como senha.

## Restrições de URL e segurança

- **Só `https`**. URLs com usuário e senha (`https://user:pass@…`) ou com `#fragmento` são recusadas. A URL tem até 2.048 caracteres.
- **Endereços internos são bloqueados**: loopback (`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.
- O host precisa resolver no DNS no momento do cadastro.
- A URL e o segredo ficam criptografados. A tela e a API mostram a URL mascarada.
- Para testar na sua máquina, exponha o servidor local com um túnel `https` público (por exemplo, ngrok ou Cloudflare Tunnel).

## Permissões

| 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.

## Limites

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            |

## Boas práticas

- **Responda rápido com 2xx.** Valide a assinatura, coloque o evento numa fila e responda. Faça o trabalho pesado depois. Uma resposta lenta vira timeout e gera nova tentativa.
- **Deduplique pelo `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.
- **Não dependa da ordem.** Compare `occurred_at` ou busque o estado atual do card quando a ordem importar.
- **Ignore eventos e campos desconhecidos.** Novos campos e eventos podem aparecer.
- **Guarde o segredo como senha**, numa variável de ambiente ou num cofre de segredos, nunca no código.
- **Acompanhe as entregas.** Ative a notificação **Webhook desativado** e confira **Ver entregas** quando algo parar de chegar.

## Perguntas frequentes

### Perdi o segredo. Como vejo de novo?

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.

### Por que não recebi o card.completed ao mover o card para Done?

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).

### Recebi o mesmo evento duas vezes. É um erro?

Não. A entrega é "pelo menos uma vez". Ignore um `id` de evento que você já processou.

### O teste funciona, mas os eventos não chegam. O que conferir?

Confira se o webhook está **Ativo**, se os **Eventos** certos estão marcados e se a mudança gera evento (veja [Regras dos eventos](#regras-dos-eventos)). Em **Ver entregas**, olhe o status, a resposta HTTP e o erro de cada entrega.

### Posso receber a descrição do card?

Não. A descrição nunca vai no payload. Use o `url` do card ou o [servidor MCP](https://app.krakendock.com/docs/mcp) para ler o card completo.

### Posso usar http ou um endereço da minha rede interna?

Não. O destino precisa ser `https` e público. Para testar localmente, use um túnel `https`.

## Veja também

- [Integração com o GitHub](https://app.krakendock.com/docs/github): importação de issues, que gera o evento `import.completed`.
- [Servidor MCP](https://app.krakendock.com/docs/mcp): leia e altere cards a partir de um assistente de IA.
- [Boards e cards](https://app.krakendock.com/docs/boards): colunas, finalizar cards e sprints.
- [Campos e templates](https://app.krakendock.com/docs/fields-templates): os campos customizados que aparecem em `custom_fields`.
