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).
GGspeak
Seção intitulada “GGspeak”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.
{ "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.
Discord
Seção intitulada “Discord”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.
{ "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(ouauthor.name, ou “Aviso”),description,url,color,fields(nameevalue),footer.textetimestamp. 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.
contentvira o texto da mensagem. Um corpo sem os camposcontenteembedsnã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,thumbnaileallowed_mentionssã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.
Cor do Discord vira estado
Seção intitulada “Cor do Discord vira estado”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.
{ "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 } ]}textvira o texto da mensagem, convertido para markdown:*negrito*vira**negrito**,<url|rótulo>vira link e&,<e>voltam a ser o caractere.- O 1º attachment que tem
colorvira 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 detitle(oufallback), o corpo detext, o link detitle_linke os campos defields(titleevalue). - Attachment sem
coloré ignorado. Se quiser o cartão, mandecolor. - Um corpo sem os campos
text,attachmentseblocksnã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
sectioneheaderviram texto (oheaderem negrito). Os outros tipos de bloco são ignorados. - Sem
key: cada POST é uma mensagem nova.
Better Stack
Seção intitulada “Better Stack”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{ "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.