Principal Configurações Webhook: mande os artigos para o seu próprio sistema

Webhook: mande os artigos para o seu próprio sistema

Última atualização em Sep 11, 2026

O que é

O webhook é um destino de publicação, como o WordPress, o Webflow, o Wix ou a Shopify, só que sem uma plataforma fixa do outro lado. Você aponta a URL do seu sistema (um CMS próprio, um Next.js, o n8n, o Make, o HubSpot, o que for) e a ClickHunt envia cada artigo lá, assinado, no formato que você escolher.

Disponível no plano Pro. Cada projeto tem um único webhook. Se você precisa mandar para dois sistemas diferentes, coloque um roteador do seu lado.

Como conectar

  1. Abra Integrações no projeto e escolha Webhook, ou selecione "Meu próprio sistema (webhook)" no wizard de criação do projeto.
  2. Cole a URL do seu endpoint. Ela precisa começar com https:// e ser pública (o safeFetch recusa IP privado, localhost e esquemas diferentes de https).
  3. Se o seu endpoint exigir algum cabeçalho fixo (por exemplo Authorization: Bearer …), digite um por linha no formato Nome: valor. Até 10 cabeçalhos. Os nomes Host, Content-Length, Content-Type, User-Agent e qualquer coisa começando com X-ClickHunt- são reservados e não podem ser usados aqui, porque a entrega já os define sozinha.
  4. Marque os eventos que você quer receber: publicado, atualizado, arquivado, excluído. Todos vêm marcados por padrão.
  5. Marque os formatos do corpo do artigo que você quer no payload: HTML, Blocos (o artigo inteiro estruturado, em JSON) e Markdown. Pode marcar mais de um ao mesmo tempo. HTML vem marcado por padrão.
  6. Clique em conectar. A ClickHunt envia um ping real para o seu endpoint; se ele responder 2xx, a conexão fica pronta.

A URL e os cabeçalhos só são definidos nesse momento. Para trocar a URL depois, desconecte e conecte de novo. Eventos e formatos você pode mudar quando quiser, sem reconectar.

O segredo

Ao conectar, a ClickHunt gera um segredo de assinatura (whsec_…) e mostra ele uma única vez, na tela, com um botão de copiar. Guarde em um lugar seguro: ele não aparece de novo depois de fechar o painel.

Esse segredo é o que garante que uma entrega realmente veio da ClickHunt, e não de alguém se passando por ela. Veja a seção "Verificar a assinatura" abaixo para o código que confere isso do seu lado.

Se você desconfiar que o segredo vazou, use Regenerar segredo na tela de configuração. O segredo antigo para de funcionar na hora e um novo é mostrado, também uma única vez.

Cabeçalhos enviados

Toda entrega (artigo, ping ou teste) chega com estes cabeçalhos:

Cabeçalho O que traz
Content-Type sempre application/json
User-Agent ClickHunt-Webhook/1
X-ClickHunt-Event o evento desta entrega (article.published, article.updated, article.archived, article.deleted, article.test ou ping)
X-ClickHunt-Delivery-Id um UUID que identifica o episódio de entrega. Numa retentativa do mesmo episódio, o id se repete, para você deduplicar do seu lado
X-ClickHunt-Api-Version a versão do formato do payload, hoje 2026-09-11
X-ClickHunt-Signature a assinatura HMAC, no formato t=<timestamp unix>,v1=<hex>

Além destes, chegam também os cabeçalhos fixos que você configurou (se houver).

Verificar a assinatura

A assinatura segue o mesmo formato do Stripe: t=<timestamp>,v1=<hmac sha256 hex>, calculado sobre "${t}.${corpo bruto da requisição}" com o seu segredo. Compare com timingSafeEqual (ou equivalente), nunca com ==, e rejeite timestamps fora de uma janela de tolerância (5 minutos é razoável) para evitar replay.

Node.js:

const crypto = require("node:crypto");

function verificar(segredo, corpoBruto, header, agoraSegundos, tolerancia = 300) {
  const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header);
  if (!m) return false;
  const t = Number(m[1]);
  const v1 = m[2];
  if (!Number.isFinite(t) || Math.abs(agoraSegundos - t) > tolerancia) return false;

  const esperado = crypto.createHmac("sha256", segredo).update(`${t}.${corpoBruto}`).digest("hex");
  const a = Buffer.from(esperado, "hex");
  const b = Buffer.from(v1, "hex");
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// Exemplo de uso num handler Express/Next:
// const ok = verificar(process.env.CLICKHUNT_WEBHOOK_SECRET, corpoBruto, req.headers["x-clickhunt-signature"], Math.floor(Date.now() / 1000));

Python:

import hashlib
import hmac
import re
import time

def verificar(segredo: str, corpo_bruto: str, header: str, agora_segundos: int, tolerancia: int = 300) -> bool:
    m = re.match(r"^t=(\d+),v1=([a-f0-9]{64})$", header)
    if not m:
        return False
    t = int(m.group(1))
    v1 = m.group(2)
    if abs(agora_segundos - t) > tolerancia:
        return False

    mensagem = f"{t}.{corpo_bruto}".encode("utf-8")
    esperado = hmac.new(segredo.encode("utf-8"), mensagem, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, v1)

# ok = verificar(SEGREDO, corpo_bruto, request.headers["X-ClickHunt-Signature"], int(time.time()))

Importante: use o corpo bruto da requisição (os bytes exatos que chegaram), antes de qualquer JSON.parse. Um framework que já reserializa o corpo antes de chegar no seu handler vai quebrar a verificação.

Payload

Este é o payload completo de um article.published, com os três formatos (HTML, Blocos e Markdown) ligados, gerado pelo mesmo artigo de exemplo que o botão "Enviar artigo de teste" usa:

{
  "event": "article.published",
  "apiVersion": "2026-09-11",
  "deliveryId": "5f0c5c0e-4b1c-4d0e-9a2b-7c3d1e2f3a4b",
  "sentAt": "2026-09-11T12:00:00.000Z",
  "test": false,
  "project": {
    "id": "00000000-0000-4000-8000-000000000002",
    "name": "Projeto de exemplo",
    "brandName": "ClickHunt",
    "website": "https://clickhunt.ai",
    "contentLanguage": "pt-br"
  },
  "article": {
    "id": "00000000-0000-4000-8000-000000000001",
    "externalId": null,
    "title": "Guia",
    "slug": "guia-exemplo",
    "previousSlugs": [],
    "status": "published",
    "contentType": "guide",
    "contentLanguage": "pt-br",
    "publishedAt": "2026-09-01T12:00:00Z",
    "updatedAt": "2026-09-05T09:00:00Z",
    "createdAt": "2026-08-28T09:00:00Z",
    "excerpt": "Desc",
    "body": {
      "html": "<p>Intro.</p>\n<blockquote><p><strong>Resposta rápida</strong></p><p>Resposta.</p></blockquote>\n<h2>Resumo</h2><ul><li>um</li><li>dois</li></ul>\n<img src=\"https:…",
      "blocks": {
        "meta": {
          "slug": "guia",
          "contentType": "guide",
          "contentLanguage": "pt-br",
          "title": "Guia",
          "metaTitle": "Guia SEO",
          "metaDescription": "Desc"
        },
        "authority": {
          "authorName": "Ana"
        },
        "entities": [],
        "references": [
          {
            "id": "r1",
            "url": "https://fonte.example/x",
            "title": "Fonte X"
          }
        ],
        "evidence": [],
        "body": [
          {
            "id": "b1",
            "type": "intro",
            "text": "Intro."
          },
          {
            "id": "b2",
            "type": "quick_answer",
            "text": "Resposta."
          }
        ]
      },
      "markdown": "Intro.\n\n> **Resposta rápida**\n>\n> Resposta.\n\n## Resumo\n\n- um\n- dois\n\n## Seção\n\nPrimeiro.\n\nVeja a [fonte](https://fonte.example/x) aqui.\n\n### Sub\n\n1. a\n2. b\n\n> C…"
    },
    "seo": {
      "metaTitle": "Guia SEO",
      "metaDescription": "Desc",
      "canonical": null,
      "redirect": null,
      "keyword": "guia de exemplo",
      "keywords": [
        "guia de exemplo"
      ],
      "jsonLd": [
        {
          "@type": "Organization",
          "@id": "https://clickhunt.ai/#organization",
          "name": "ClickHunt",
          "url": "https://clickhunt.ai"
        },
        {
          "@type": "BlogPosting",
          "headline": "Guia",
          "description": "Desc",
          "image": [
            "https://app.clickhunt.ai/logos/platforms/clickhunt.png"
          ],
          "datePublished": "2026-09-01T12:00:00Z",
          "dateModified": "2026-09-05T09:00:00Z",
          "inLanguage": "pt-br",
          "articleSection": [
            "exemplo"
          ],
          "keywords": "guia de exemplo, exemplo",
          "citation": [
            {
              "@type": "CreativeWork",
              "name": "Fonte X",
              "url": "https://fonte.example/x"
            }
          ],
          "author": {
            "@type": "Person",
            "name": "Ana Souza",
            "jobTitle": "Editora",
            "description": "Escreve sobre visibilidade em IA e SEO na ClickHunt.",
            "image": "https://app.clickhunt.ai/logos/platforms/clickhunt.png",
            "sameAs": [
              "https://www.linkedin.com/company/clickhunt"
            ]
          },
          "publisher": {
            "@id": "https://clickhunt.ai/#organization"
          }
        }
      ]
    },
    "taxonomy": {
      "categories": [
        "Guia"
      ],
      "tags": [
        "exemplo"
      ]
    },
    "author": {
      "name": "Ana Souza",
      "role": "Editora",
      "bio": "Escreve sobre visibilidade em IA e SEO na ClickHunt.",
      "avatar": "https://app.clickhunt.ai/logos/platforms/clickhunt.png",
      "social": {
        "linkedin": "https://www.linkedin.com/company/clickhunt"
      }
    },
    "createdBy": {
      "name": "Ana Souza",
      "avatar": "https://app.clickhunt.ai/logos/platforms/clickhunt.png"
    },
    "cover": {
      "url": "https://app.clickhunt.ai/logos/platforms/clickhunt.png",
      "alt": "Capa do artigo"
    },
    "images": [
      {
        "url": "https://app.clickhunt.ai/logos/platforms/clickhunt.png",
        "alt": "Capa do artigo",
        "caption": null,
        "width": null,
        "height": null,
        "kind": "cover",
        "position": -1
      },
      {
        "url": "https://cdn.example/a.webp",
        "alt": "Alt A",
        "caption": "Legenda",
        "width": 800,
        "height": 600,
        "kind": "stock",
        "position": 3
      }
    ],
    "cta": {
      "id": "cta-exemplo",
      "headline": "Quer rastrear sua marca nas IAs?",
      "subheadline": "Veja como a ClickHunt monitora ChatGPT, Claude, Gemini e Perplexity.",
      "buttonLabel": "Conhecer a ClickHunt",
      "buttonLink": "https://clickhunt.ai",
      "utm": {
        "source": "clickhunt",
        "medium": "webhook-example",
        "campaign": "guia-exemplo",
        "content": "",
        "term": ""
      }
    },
    "generation": {
      "origin": "manual",
      "objective": null,
      "goal": null,
      "length": null,
      "country": null,
      "keyword": "guia de exemplo",
      "keywords": [
        "guia de exemplo"
      ],
      "imageStyle": null,
      "imagePalette": null,
      "autoLink": null,
      "materials": [],
      "references": []
    }
  }
}

Exemplo abreviado: html, markdown, blocks.body e images foram encurtados para caber aqui. O botão Enviar artigo de teste entrega o payload completo no seu endpoint.

Alguns pontos sobre este payload que valem a pena notar:

  • body só traz as chaves dos formatos que você marcou na configuração (html, blocks, markdown). Se você desmarcar todos menos um, os outros dois somem do corpo, não vêm vazios. Quando o artigo foi editado à mão no editor, html e markdown refletem o texto editado e blocks mantém a estrutura gerada.
  • article.deleted manda um corpo bem mais enxuto: só { id, externalId, slug, previousSlugs }, sem o resto do artigo, porque nessa hora o conteúdo pode já ter sido apagado daqui também.
  • cta.utm pode conter os textos literais {project} e {article} em medium/campaign/content quando o CTA usa o padrão da plataforma. Isso é um modelo, não um valor pronto: substitua {project} pelo nome (ou id) do projeto e {article} pelo identificador do artigo antes de montar o link final do seu lado.
  • article.generation.goal e article.generation.length vêm sempre null hoje. O produto ainda não coleta esses dois campos na criação do artigo; quando passar a coletar, eles deixam de vir vazios, sem mudar o formato do payload.
  • Um artigo editado várias vezes seguidas em menos de 15 segundos gera um único article.updated no final da rajada, não um por edição salva.

Eventos e quando disparam

O evento que sai depende do estado atual do artigo aqui e do que o seu sistema já recebeu antes:

Situação aqui O que o seu sistema já tinha Evento enviado
Artigo publicado Ainda não tinha esse artigo article.published
Artigo publicado (edição) Já tinha o artigo publicado article.updated
Artigo arquivado ou volta para rascunho Tinha o artigo publicado article.archived (o payload leva o status real)
Artigo arquivado ou rascunho Nunca chegou a receber o artigo nenhum evento (nada a derrubar)
Artigo excluído aqui Tinha o artigo publicado article.deleted
Artigo excluído aqui Nunca recebeu esse artigo nenhum evento

Rascunho nunca é enviado como novidade. Mas se um artigo que já está no seu sistema volta para rascunho por aqui, você recebe um article.archived avisando, porque do contrário as duas pontas ficariam divergentes em silêncio.

Se você desligar um evento na configuração (por exemplo, desligar "publicado"), esse artigo específico nunca dispara aquele evento, e como consequência os eventos seguintes dele também ficam presos: sem article.published, o article.updated daquele mesmo artigo nunca vai sair, porque para o sistema o artigo nunca chegou a ser publicado do seu lado.

Além dos quatro eventos de artigo, existem dois auxiliares: ping (o teste de conexão, sem article no corpo) e article.test (o "Enviar artigo de teste" do painel, com test: true e a fixture de exemplo).

O que devolver

Basta responder com um status 2xx. Redirecionamentos (3xx) são tratados como falha: a URL configurada precisa responder diretamente, sem redirecionar para outro endereço. O corpo é opcional; se você quiser, devolva um JSON com { "id": "...", "url": "..." } (ou { "data": { "id": "...", "url": "..." } }), e a ClickHunt guarda esse id como o identificador do artigo no seu sistema e essa url como o link público dele. Qualquer outro formato de resposta é ignorado, sem erro.

Fora da faixa 2xx, o que sai muda o que a ClickHunt registra:

Situação Código interno
Timeout, DNS, falha de conexão network
URL barrada por segurança (IP privado, esquema inválido) blocked_url
401, 403 ou outro 4xx endpoint_rejected
404 not_found
410 endpoint_gone
429 rate_limited
5xx endpoint_error

O prazo de resposta é de 15 segundos. Se o seu endpoint só enfileira a requisição (por exemplo, um serverless atrás de uma fila), responda 2xx assim que enfileirar, sem esperar o processamento terminar.

Retentativas

Uma entrega que falhar é tentada de novo, com um intervalo que cresce a cada tentativa: 5 minutos depois da 1ª falha, 15 minutos depois da 2ª, 1 hora depois da 3ª, 6 horas depois da 4ª. São 5 tentativas ao todo (a original mais 4 retentativas), o que leva cerca de 7 horas e 20 minutos entre a 1ª e a 5ª falha. Na 5ª falha, a ClickHunt para de tentar sozinha, sem chegar ao quinto degrau do backoff que outras plataformas continuam usando.

Depois de parar, o artigo fica com o estado de falha visível na tela, e só volta a tentar quando você clicar em Reenviar, seja na tela do artigo ou na aba Histórico.

Histórico

A aba Histórico do painel do webhook mostra as últimas 50 entregas: evento, artigo, código HTTP, duração, número da tentativa e data. Clicar numa linha expande a resposta que o seu endpoint devolveu.

Cada linha tem um botão de Reenviar, quando faz sentido: para article.published, article.updated e article.archived, ele limpa o erro e enfileira o artigo de novo; para ping, ele testa a conexão de novo; para article.test, ele reenvia o mesmo artigo de exemplo. article.deleted não tem botão de reenvio.

O histórico é mantido por 30 dias; entradas mais antigas são removidas automaticamente.

Perguntas frequentes

Um artigo em rascunho é enviado? Não. Rascunho nunca sai pelo webhook. Só publicado, atualizado, arquivado e excluído disparam entrega, e mesmo assim só os eventos que você deixou marcados.

Como testo num ambiente local, antes de subir meu endpoint? Use um túnel como o ngrok (ou similar) para expor a sua máquina numa URL https pública temporária, cole essa URL na configuração do webhook e use o botão "Enviar artigo de teste" para validar a assinatura e o formato sem publicar nada de verdade.

Se eu conectar o webhook hoje, os artigos que já publiquei antes são reenviados? Não. O webhook só entrega artigos novos a partir do momento em que ele é conectado. Não existe reenvio automático do que já estava publicado no destino anterior (blog nativo ou outra plataforma).