Pular para o conteúdo

Formatos

O GGspeak lê quatro formatos. Os três primeiros são detectados pelo corpo; o do Better Stack só pelo sufixo /betterstack na URL. Qualquer formato pode ser forçado pelo sufixo (/discord, /slack, /betterstack).

Formato Reconhece por Vira
GGspeak card ou key no topo Cartão nativo
Discord content ou embeds O 1º embed vira cartão; os outros viram markdown no texto
Slack text, attachments ou blocks Texto em markdown; o 1º attachment com color dá o estado do cartão
Better Stack Sufixo /betterstack Cartão do incidente, com a key igual ao id dele

Com um sufixo (/discord, /slack, /betterstack), o corpo precisa ter os campos daquele formato. Na URL sem sufixo, a detecção segue a ordem GGspeak, Discord, Slack; se nenhum casar, a resposta é 422 com payload_unrecognized.

Em todos os formatos, as menções do payload viram texto simples: @everyone, @here, <@id>, <@&id> e os tokens de menção do GGspeak são escapados antes de gravar. Quem chama é só o cargo do webhook (Chamar um cargo).

O formato nativo, com tudo o que o cartão sabe fazer. É o único que usa key e state direto. Um text em markdown só é lido como GGspeak junto com card ou key; um corpo só com text é lido como Slack.

Exemplo completo
{
"key": "deploy-master-4856e7ac",
"text": "Deploy da API terminou.",
"card": {
"title": "Deploy da API: master",
"state": "ok",
"body": "api: criar, listar, pausar, apagar e gerar novo link de webhook",
"url": "https://github.com/ggspeak/ggspeak/actions/runs/11800234567",
"fields": [
{ "name": "Commit", "value": "4856e7ac" },
{ "name": "Quem", "value": "rafael" },
{ "name": "Duração", "value": "3 min 12 s" }
]
}
}
Campo O que é
text Texto em markdown, opcional. Até 2.000 caracteres: acima disso a API recusa em vez de cortar.
card O cartão, opcional. text ou card é obrigatório.
key Opcional. Até 128 caracteres ASCII visíveis, sem espaço. Quem manda a mesma key atualiza o mesmo cartão: veja Cartão e key. A key só vale junto com card: com só text, cada POST cria uma mensagem nova.
username Opcional, até 32 caracteres. O nome que aparece naquela mensagem, no lugar do nome do webhook.
card.title Obrigatório. Até 256 caracteres.
card.state alert, warn, ok ou info. Sem ele, info.
card.body Markdown, até 1.000 caracteres.
card.url Link http:// ou https:// de até 512 caracteres. O que não for isso é descartado.
card.fields Até 6 campos { "name", "value" }: nome até 64 caracteres, valor até 256. Os que passam de 6 são ignorados.

Os textos do cartão que passam do limite são cortados com “…”. Tipo errado (state fora da lista, fields que não é lista, campo sem name ou value) volta como payload_invalid, com o campo no message.

O estado aparece com ícone e palavra: alert é “Disparou”, warn é “Aviso”, ok é “Resolvido” e info é “Info”. A cor sai do tema do app, não do payload.

Se a ferramenta já manda para um webhook do Discord, troque só a URL. O exemplo abaixo é o que o contact point Discord do Grafana manda.

Corpo do Discord (Grafana)
{
"username": "Grafana",
"content": "",
"embeds": [
{
"title": "[FIRING:1] HighCPU (sp-1 api)",
"url": "https://grafana.ggspeak.app/alerting/list?view=list&search=HighCPU",
"color": 15158332,
"description": "**Firing**\n\nValue: A=92.4\nLabels:\n - alertname = HighCPU\n - host = sp-1\n - job = api\nAnnotations:\n - summary = CPU acima de 90% há 5 minutos\nSource: https://grafana.ggspeak.app/alerting/grafana/cpu-high/view\nSilence: https://grafana.ggspeak.app/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DHighCPU",
"footer": {
"text": "Grafana v11.3.0",
"icon_url": "https://grafana.com/static/assets/img/fav32.png"
}
}
]
}
  • O 1º embed vira o cartão: title (ou author.name, ou “Aviso”), description, url, color, fields (name e value), footer.text e timestamp. O rodapé e a data entram no fim do corpo do cartão.
  • Os embeds seguintes viram markdown no fim do texto: título em negrito e a descrição abaixo.
  • content vira o texto da mensagem. Um corpo sem os campos content e embeds não é Discord: com o sufixo /discord, ou quando não há campo de outro formato (na URL sem sufixo a detecção vai de GGspeak a Discord a Slack), a resposta é payload_unrecognized. Se o campo existe mas está vazio ("content": "" sem embeds), é payload_invalid.
  • username é o nome daquela mensagem (até 32 caracteres).
  • avatar_url, image, thumbnail e allowed_mentions são ignorados. O GGspeak não busca imagem nem avatar de URL de terceiros, então nenhum IP vaza por aí.
  • Pelo Discord não há key: cada POST é uma mensagem nova.

O color do Discord é um número (RGB). A matiz decide o estado do cartão:

Cor Estado
Vermelho e magenta alert
Laranja e amarelo warn
Verde ok
O resto (azul, ciano, roxo) e sem cor info

Cinza e cores muito apagadas ou muito escuras caem em info. O vermelho 15158332 do exemplo vira alert; 3066993 (verde) vira ok.

O corpo de um webhook do Slack: text, attachments e blocks.

Corpo do Slack
{
"text": "*Backup diário* terminou com aviso <https://status.ggspeak.app/backup|ver detalhes>",
"attachments": [
{
"fallback": "Backup diário: 2 tabelas acima do tamanho esperado",
"color": "warning",
"title": "Backup diário",
"title_link": "https://status.ggspeak.app/backup",
"text": "2 tabelas acima do tamanho esperado: `channel_messages` e `hook_deliveries`.",
"fields": [
{ "title": "Tamanho", "value": "1,8 GB", "short": true },
{ "title": "Duração", "value": "42 s", "short": true }
],
"footer": "pg_dump",
"ts": 1793700000
}
]
}
  • text vira o texto da mensagem, convertido para markdown: *negrito* vira **negrito**, <url|rótulo> vira link e &amp;, &lt; e &gt; voltam a ser o caractere.
  • O 1º attachment que tem color vira o cartão. good é ok, warning é warn, danger é alert, e um hexadecimal (#e5484d) segue a mesma regra de matiz do Discord. O título vem de title (ou fallback), o corpo de text, o link de title_link e os campos de fields (title e value).
  • Attachment sem color é ignorado. Se quiser o cartão, mande color.
  • Um corpo sem os campos text, attachments e blocks não é Slack: com o sufixo /slack, ou quando não há campo de outro formato, a resposta é payload_unrecognized. Com o campo presente mas sem nada para mostrar, é payload_invalid.
  • Blocos section e header viram texto (o header em negrito). Os outros tipos de bloco são ignorados.
  • Sem key: cada POST é uma mensagem nova.

O Better Stack não manda state; o corpo padrão é o incidente. Use a URL com o sufixo /betterstack:

https://ggspeak.app/hooks/{id}/{token}/betterstack
Incidente disparado
{
"data": {
"id": "567890123",
"type": "incident",
"attributes": {
"name": "API",
"url": "https://uptime.betterstack.com/team/123456/incidents/567890123",
"http_method": "get",
"cause": "Status 503",
"incident_group_id": null,
"started_at": "2026-11-03T14:02:00.000Z",
"acknowledged_at": null,
"acknowledged_by": null,
"resolved_at": null,
"resolved_by": null,
"status": "Started",
"team_name": "GGspeak",
"response_content": "",
"response_options": null,
"regions": ["us", "eu"],
"response_url": null,
"screenshot_url": null,
"escalation_policy_id": null,
"call": true,
"sms": true,
"email": true,
"push": true
},
"relationships": {
"monitor": { "data": { "id": "2345678", "type": "monitor" } }
}
}
}
O que o GGspeak lê Vira
data.id A key do cartão: todo o ciclo do incidente cai no mesmo cartão
attributes.resolved_at preenchido ok
attributes.acknowledged_at preenchido (e sem resolved_at) warn
nenhum dos dois alert
attributes.name Título (sem ele, “Incidente”)
attributes.cause Corpo
attributes.url Link do cartão

O resto do corpo é ignorado. O mesmo incidente reconhecido tem acknowledged_at e acknowledged_by, e o resolvido tem também resolved_at e resolved_by. O resultado é o cartão que vai de “Disparou” a “Aviso” e “Resolvido” sem encher o canal.