Receber eventos por webhook
O canal Webhook deixa outro sistema — a sua loja virtual, o sistema de cobrança, a central de chamados — avisar a plataforma quando algo acontece: uma compra feita, um pagamento aprovado, um chamado aberto. O sistema de origem faz uma chamada HTTP para um endereço seu; a plataforma confere quem chamou, recebe o aviso e, se você quiser, aciona um agente para cuidar dele — e pode devolver a resposta do agente ao seu sistema.
Aqui ninguém "conversa": quem fala é um sistema. Cada chamada é um aviso (também chamado de evento), e a tela usa a palavra aviso.
Use o webhook quando você quiser:
- ver o que acontece em outros sistemas, aviso por aviso, sem escrever código do seu lado;
- deixar um agente agir sozinho a cada aviso — por exemplo, conferir um pedido ou preparar uma resposta, com as ferramentas liberadas ao público;
- receber a resposta do agente de volta no sistema de origem, para mostrá-la ao cliente ou gravá-la no pedido;
- testar a integração com segurança antes de ligar o agente.
Como funciona
A plataforma só responde 200 com accepted depois de guardar o aviso com
segurança. O agente (ou o registro na tela) cuida dele em seguida, sem fazer o
sistema de origem esperar. No modo acionar, a plataforma pode também enviar a
resposta do agente ao seu sistema: veja
Receber a resposta do agente.
Criar um webhook
Só administradores da organização criam e alteram webhooks. No celular, a tela é só de leitura: use o computador.
- No painel, abra Configurações → Canais e vá até a seção Webhook.
- Clique em Novo webhook e preencha:
- Nome — para você reconhecer na lista, por exemplo Pedidos da loja;
- Agente — quem recebe os avisos deste webhook;
- Autenticação — como o sistema de origem prova quem é (veja abaixo);
- Instrução (opcional) — o que o agente deve fazer com cada aviso;
- Quem pode enviar (opcional) — uma lista de endereços de IP;
- Opções avançadas (opcional) — como ler o corpo de cada aviso (veja abaixo).
- Clique em Criar webhook.
- Aparece o cartão Webhook criado, com o Endereço (URL) do webhook e, conforme a autenticação escolhida, o Segredo da assinatura, o Token ou a Senha. Copie tudo e clique em Já copiei. (Se você colou o segredo da assinatura, ele não é mostrado de volta: você já o tem.)
- Cole o endereço no sistema de origem, como o destino dos avisos.
A URL e os segredos gerados aparecem uma única vez: depois disso, não há como vê-los de novo. Se perder, use Trocar a URL (ou troque a autenticação) no detalhe do webhook.
Todo webhook nasce no modo só registrar: os avisos chegam e aparecem na Atividade (um resumo dos mais recentes, não um histórico completo), e o agente não age sozinho. Quando você estiver satisfeito com o que chega, passe para acionar o agente.
A URL e a chave
A URL do webhook termina em /v1/hooks/ seguido da chave:
https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI
A tela mostra a URL inteira ao criar o webhook: copie e cole essa no sistema de
origem. Nos exemplos desta página, SUA_CHAVE_AQUI é de mentira — troque pela
chave da sua URL.
- O sistema de origem sempre envia com o método
POST. - A última parte do endereço é a chave: ela identifica o webhook e, sozinha, já é uma credencial. É longa e impossível de adivinhar, mas quem tem a URL consegue enviar avisos — trate-a como uma senha e não a compartilhe.
- Trocar a URL (no detalhe do webhook) gera uma chave nova e derruba a anterior na hora. A autenticação escolhida continua a mesma. O sistema de origem precisa receber o endereço novo; até lá, os avisos deixam de chegar.
- Apagar o webhook desliga a URL de vez.
Autenticação
A chave da URL sempre vale. A Autenticação acrescenta uma segunda conferência, e você escolhe entre quatro tipos:
| Na tela | Identificador | O sistema de origem precisa… |
|---|---|---|
| Só a chave da URL | url_key | Enviar para a URL. Mais nada. |
| Assinatura | hmac | Assinar cada aviso com um segredo combinado e mandar a assinatura num cabeçalho. É a opção mais segura, quando o sistema de origem sabe assinar. |
| Token no cabeçalho | header | Mandar um token num cabeçalho de cada chamada. |
| Usuário e senha | basic | Mandar usuário e senha no cabeçalho Authorization (Basic). |
Os exemplos usam chave e segredos de mentira (SUA_CHAVE_AQUI,
segredo-de-exemplo): troque pelos seus.
Só a chave da URL
O mais simples: quem tem a URL envia.
curl -X POST "https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{"type": "pedido.criado", "id": "evt_123", "data": {"pedido": "1001"}}'
A resposta é 200:
{"status": "accepted", "eventId": "3f2b8c1a-5d4e-4a7b-9c10-2e6d8f0a1b34"}
Assinatura (HMAC)
O sistema de origem calcula uma assinatura do corpo do aviso com um segredo combinado e a manda num cabeçalho. A plataforma refaz a conta e só aceita se bater. Na tela você informa:
| Campo | O que é |
|---|---|
| Cabeçalho da assinatura | O nome do cabeçalho que leva a assinatura, por exemplo X-Hub-Signature-256. |
| Algoritmo da assinatura | SHA-256 (o mais comum), SHA-1 ou SHA-512. |
| Formato da assinatura | Hexadecimal (o mais comum) ou Base64. |
| Prefixo antes da assinatura (opcional) | O que o sistema de origem escreve antes da assinatura, como sha256=. |
| Segredo que o sistema de origem gerou (opcional) | Se o sistema de origem já tem um segredo, cole aqui. Em branco, a plataforma gera um e mostra uma única vez. |
Como a assinatura é conferida:
- A conta é o HMAC do corpo exatamente como foi enviado, byte a byte. Assine os mesmos bytes que você manda: se o seu código monta o JSON, assine o texto final, e não o objeto de novo serializado (espaços e ordem dos campos mudam a assinatura).
- O nome do cabeçalho não diferencia maiúsculas de minúsculas. Em hexadecimal,
letras maiúsculas ou minúsculas servem; em Base64, use o Base64 comum, com
=no fim (não a variante para URLs, com-e_). - Se você configurou um prefixo, o valor do cabeçalho precisa começar
exatamente com ele (
sha256=seguido da assinatura). - Corpo alterado em um único byte, assinatura de outro segredo ou cabeçalho
ausente resultam em
401.
Os exemplos abaixo supõem: cabeçalho X-Hub-Signature-256, algoritmo SHA-256,
formato hexadecimal, prefixo sha256= e segredo segredo-de-exemplo.
Python
import hashlib
import hmac
import urllib.request
SEGREDO = "segredo-de-exemplo"
URL = "https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI"
# O texto exato que será enviado: a assinatura é calculada sobre estes bytes.
corpo = '{"type": "pedido.criado", "id": "evt_123", "data": {"pedido": "1001"}}'.encode("utf-8")
assinatura = hmac.new(SEGREDO.encode("utf-8"), corpo, hashlib.sha256).hexdigest()
pedido = urllib.request.Request(
URL,
data=corpo,
method="POST",
headers={
"Content-Type": "application/json",
"X-Hub-Signature-256": f"sha256={assinatura}",
},
)
with urllib.request.urlopen(pedido) as resposta:
print(resposta.status, resposta.read().decode("utf-8"))
Node.js
Salve como enviar.mjs e rode com node enviar.mjs (Node 18 ou mais novo, sem
instalar nada):
import crypto from "node:crypto";
const SEGREDO = "segredo-de-exemplo";
const URL = "https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI";
// O texto exato que será enviado: a assinatura é calculada sobre estes bytes.
const corpo = '{"type": "pedido.criado", "id": "evt_123", "data": {"pedido": "1001"}}';
const assinatura = crypto.createHmac("sha256", SEGREDO).update(corpo, "utf8").digest("hex");
const resposta = await fetch(URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hub-Signature-256": `sha256=${assinatura}`,
},
body: corpo,
});
console.log(resposta.status, await resposta.text());
Linha de comando
CORPO='{"type": "pedido.criado", "id": "evt_123", "data": {"pedido": "1001"}}'
ASSINATURA=$(printf '%s' "$CORPO" | openssl dgst -sha256 -hmac "segredo-de-exemplo" | sed 's/^.* //')
curl -X POST "https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=$ASSINATURA" \
-d "$CORPO"
Se o formato escolhido for Base64, a assinatura muda de forma, e só ela:
import base64
assinatura = base64.b64encode(
hmac.new(SEGREDO.encode("utf-8"), corpo, hashlib.sha256).digest()
).decode("ascii")
const assinatura = crypto.createHmac("sha256", SEGREDO).update(corpo, "utf8").digest("base64");
Para SHA-1 ou SHA-512, troque o algoritmo nos dois lados (hashlib.sha1 ou
hashlib.sha512 no Python; "sha1" ou "sha512" no Node.js).
Token no cabeçalho
O sistema de origem manda um token (uma senha) num cabeçalho de cada chamada. Na tela você informa:
| Campo | O que é |
|---|---|
| Nome do cabeçalho do token | O cabeçalho que leva o token, por exemplo Authorization ou X-Token. |
| Começo do valor (opcional) | O que vem antes do token, como Bearer (com o espaço no final). |
A plataforma gera o token e mostra uma única vez ao criar o webhook. Guarde-o no cofre de segredos do sistema de origem.
curl -X POST "https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SEU_TOKEN_AQUI" \
-d '{"type": "pedido.criado", "id": "evt_123", "data": {"pedido": "1001"}}'
O exemplo supõe o cabeçalho Authorization e o começo do valor Bearer .
Usuário e senha
O sistema de origem manda um usuário e uma senha no cabeçalho Authorization,
no formato Basic. Na tela você informa só o Usuário; a plataforma gera a
senha e a mostra uma única vez.
curl -X POST "https://api.lab019.ai/gateway/v1/hooks/SUA_CHAVE_AQUI" \
-u "usuario-de-exemplo:SUA_SENHA_AQUI" \
-H "Content-Type: application/json" \
-d '{"type": "pedido.criado", "id": "evt_123", "data": {"pedido": "1001"}}'
Se o usuário ou a senha estiverem errados, a resposta é 401 com o cabeçalho
WWW-Authenticate: Basic realm="webhook".
Trocar a autenticação
- Trocar o tipo de autenticação gera um segredo novo (uma senha ou um token), mostrado uma única vez depois de salvar. O segredo anterior deixa de valer na hora.
- Passar para Só a chave da URL apaga o segredo guardado.
- Editar o webhook sem mudar o tipo mantém o segredo atual.
Quem pode enviar: filtro de IP
Em Quem pode enviar (opcional) você lista endereços de IP, um por linha ou
separados por vírgula, como 203.0.113.7 ou 198.51.100.0/24. Em branco,
qualquer endereço pode enviar. Com a lista preenchida, O que a lista faz
define como ela vale:
- Só estes endereços podem enviar — qualquer outro endereço recebe
403; - Estes endereços são bloqueados — os da lista recebem
403, os demais passam.
O filtro de IP vale junto com a autenticação, não no lugar dela.
O que o sistema de origem envia
- Método
POSTpara a URL do webhook. - Cabeçalho
Content-Type: application/json— ouapplication/x-www-form-urlencoded, se o webhook estiver configurado como Formulário (veja Tipo de conteúdo abaixo). - Corpo em UTF-8, sem compressão. No formato JSON, a raiz do corpo precisa
ser um objeto (
{ ... }). - Tamanho de até 256 KiB por padrão. O limite é configurável em Tamanho
máximo do aviso (bytes), de 1024 a 1048576 (1 MiB). Um corpo maior recebe
413.
Como a plataforma lê cada aviso
Tudo isto fica em Opções avançadas e é opcional. Deixando tudo em branco, o corpo inteiro vai para o agente e cada aviso é aceito.
| Campo na tela | O que faz |
|---|---|
| Tipo de conteúdo | Como o sistema de origem manda o corpo: JSON (o padrão; aceita application/json e tipos terminados em +json) ou Formulário (form-urlencoded). Um Content-Type diferente do configurado recebe 415. Em formulário, um campo repetido vira uma lista. |
| Onde está o tipo do aviso | De onde a plataforma lê o tipo do aviso, por exemplo body:type ou header:X-Event. |
| Onde está o id da entrega | De onde ela lê o identificador único do aviso. Serve para não processar o mesmo aviso duas vezes. |
| Onde está a chave de correlação | Avisos com a mesma chave caem na mesma conversa do agente, por exemplo body:data.customer.id. Em branco, cada aviso abre uma conversa nova. |
| Tipos de aviso aceitos | Uma lista (um por linha ou separados por vírgula), por exemplo pedido.criado. Em branco, aceita todos. Para usar a lista, preencha também Onde está o tipo do aviso: com esse campo em branco, a plataforma não reconhece o tipo de nenhum aviso e todos voltam 200 com ignored. Um aviso de outro tipo — ou sem tipo reconhecível — é ignorado: a resposta é 200 com ignored e nada é registrado. O Schema é conferido antes do tipo: um aviso de outro tipo que não cumpra o schema recebe 422, e não ignored. |
| Campos enviados ao agente | Os caminhos que seguem adiante, sem body:, por exemplo data.order. Em branco, vai o corpo inteiro (menos os descartados). |
| Campos descartados | Caminhos removidos antes de qualquer gravação: nem o agente, nem a tela, nem os avisos que a plataforma guarda chegam a ver. Use o caminho completo, por exemplo data.customer.cpf. |
| Schema (opcional) | Um JSON Schema que o corpo precisa cumprir. Se não cumprir, a resposta é 422. Aceita JSON Schema, menos as palavras pattern, patternProperties e $ref que aponte para fora do próprio schema. A palavra format não é verificada, e uma conferência que passe de 2 segundos também recebe 422. |
| Tamanho máximo do aviso (bytes) | De 1024 a 1048576. O padrão é 262144 (256 KiB). |
Como escrever um caminho:
body:seguido do caminho com pontos:body:data.customer.id. Para um item de lista, use o número da posição, começando em zero:body:data.items.0.id.header:seguido do nome do cabeçalho:header:X-GitHub-Event.- Só entram valores simples (texto, número ou verdadeiro/falso), com até 256 caracteres. Um objeto, uma lista ou um campo vazio contam como "sem valor".
- Campos enviados e Campos descartados usam o caminho sem o prefixo
body:. Num campo descartado, um caminho sem posição vale para cada item da lista:data.cartoes.numeroapaganumerode todos os cartões. - Um caminho que Campos descartados apague não pode ser usado como tipo, id da entrega ou chave de correlação: a plataforma recusa essa configuração ao salvar.
Se o corpo costuma trazer CPF, telefone ou cartão, descarte esses campos. Ao preencher Campos descartados, a tela sugere os mais comuns ("Costuma ter dado pessoal? Descarte…"). Veja também LGPD.
Exemplo
O sistema de origem manda:
{
"type": "pedido.criado",
"id": "evt_123",
"data": {
"pedido": "1001",
"total": 129.9,
"customer": { "id": "cli_77", "nome": "Maria", "cpf": "000.000.000-00" }
}
}
Com esta configuração:
| Campo | Valor |
|---|---|
| Onde está o tipo do aviso | body:type |
| Onde está o id da entrega | body:id |
| Onde está a chave de correlação | body:data.customer.id |
| Tipos de aviso aceitos | pedido.criado, pedido.pago |
| Campos enviados ao agente | type e data |
| Campos descartados | data.customer.cpf |
O aviso é aceito (o tipo está na lista), todos os avisos do cliente cli_77
caem na mesma conversa, e o que o agente (e a tela) recebem é:
{
"type": "pedido.criado",
"data": {
"pedido": "1001",
"total": 129.9,
"customer": { "id": "cli_77", "nome": "Maria" }
}
}
O tipo, o id da entrega e a chave de correlação são lidos do corpo original, antes do filtro; o schema também confere o corpo original.
Depois do filtro, o conteúdo guardado e entregue ao agente tem no máximo 32 KiB. Passando disso, o agente recebe só o começo, marcado como cortado. Para corpos grandes, use Campos enviados ao agente e deixe só o que importa.
Respostas
A plataforma responde com um código HTTP e, quase sempre, um JSON curto. Os
erros trazem um detail com um código curto; a plataforma não diz
qual conferência de autenticação falhou.
| Código | Resposta | Quando acontece | O que fazer |
|---|---|---|---|
200 | {"status":"accepted","eventId":"…"} | O aviso foi recebido e guardado com segurança. | Nada: deu certo. Não reenvie. |
200 | {"status":"duplicate","eventId":"…"} | Já recebemos um aviso com o mesmo id de entrega nas últimas 24 horas. | Nada: o aviso original já está recebido. Não reenvie. |
200 | {"status":"ignored"} | O tipo do aviso não está em Tipos de aviso aceitos, e o aviso cumpre o Schema (o schema é conferido primeiro). | Nada: foi descartado de propósito. Se não era para ser, inclua o tipo na lista. |
400 | {"detail":"invalid_body"} | O corpo não pode ser lido como o tipo configurado: JSON inválido, raiz que não é um objeto, aninhamento excessivo ou formulário ilegível. | Não reenvie igual. Corrija o corpo. |
401 | {"detail":"unauthorized"} | A autenticação faltou ou está errada: assinatura, token ou usuário e senha. | Não reenvie igual. Confira o segredo, o nome do cabeçalho, o prefixo e o formato. |
403 | {"detail":"forbidden"} | O endereço de IP de origem não passa pela lista de Quem pode enviar. | Não reenvie igual. Ajuste a lista ou envie de um endereço permitido. |
404 | sem corpo | A URL não existe: chave errada, URL trocada ou webhook apagado. Por segurança, a plataforma não diz qual dos casos. | Não reenvie. Confira a URL; se ela foi trocada, use a nova. |
413 | {"detail":"payload_too_large"} | O corpo é maior que o Tamanho máximo do aviso. | Não reenvie igual. Envie menos dados ou aumente o limite (até 1 MiB). |
415 | {"detail":"unsupported_media_type"} | O Content-Type não é o configurado, ou o corpo veio comprimido (Content-Encoding). | Não reenvie igual. Corrija o cabeçalho e envie sem compressão. |
422 | {"detail":"schema_mismatch","errors":[…]} | O corpo não cumpre o Schema. errors lista até 5 problemas, cada um com path e message. | Não reenvie igual. Corrija o corpo, ou o schema, conforme errors. |
429 | {"detail":"rate_limited"} e Retry-After: 60 | Muitos avisos de uma vez: o limite é de 600 por minuto para cada webhook e de 600 chamadas por minuto para cada endereço de IP de origem. | Reenvie depois de esperar o Retry-After (60 segundos). |
500 | {"detail":"internal_error"} | Falha inesperada da plataforma. | Reenvie mais tarde. |
503 | {"detail":"queue_unavailable"} ou {"detail":"schema_busy"}, com Retry-After: 5 | A plataforma não conseguiu guardar o aviso agora, ou a conferência do schema estava ocupada. Nada foi aceito. | Reenvie depois de esperar o Retry-After (5 segundos), de preferência com espera crescente. |
Em resumo:
2xx— deu certo; não reenvie, mesmo noduplicatee noignored.429,500e503— reenvie depois de esperar. É seguro: com o id da entrega configurado, um aviso que já chegou volta comoduplicate, nunca é processado duas vezes.400,401,403,404,413,415e422— reenviar igual não adianta: corrija a causa primeiro.
Um exemplo de 422:
{
"detail": "schema_mismatch",
"errors": [
{ "path": "data.total", "message": "'abc' is not of type 'number'" }
]
}
Na tela, a área Atividade do webhook conta cada desfecho: Recebidos,
Registrados, Ignorados, Recusados (400, 415, 422), Falhas de
autenticação (401, 403), Acionou o agente e Não entregues (os dois
últimos, do modo acionar) e, quando passam de zero, Repetidos, Grandes
demais (413) e Acima do limite (429 por webhook). Nem tudo é contado: as
respostas 404 e o 429 por endereço de IP não entram em nenhum contador, e um
503 entra só em Recebidos, sem desfecho. É o primeiro lugar para olhar
quando algo não chega.
Não processar o mesmo aviso duas vezes
Sistemas de origem reenviam avisos — quando a resposta demora, quando houve um erro, ou por garantia. Para o mesmo aviso não ser processado duas vezes, a plataforma usa o id da entrega:
- Configure Onde está o id da entrega com o campo que identifica o aviso,
por exemplo
header:X-GitHub-Deliveryoubody:id. - Um segundo aviso com o mesmo id, no mesmo webhook, em até 24 horas,
recebe
200comduplicatee não é processado de novo. A resposta traz o mesmoeventIddo primeiro. - O id vale por webhook: o mesmo id em dois webhooks diferentes não se confunde.
- Só avisos aceitos gastam o id. Um aviso que foi ignorado, recusado
(
400,401,413,415,422…) ou que recebeu503não impede o reenvio depois de corrigido.
Em branco, a plataforma usa uma marca do corpo inteiro: dois avisos com o corpo idêntico, byte a byte, em até 24 horas, contam como o mesmo aviso. Se o seu sistema pode mandar dois avisos diferentes com o mesmo corpo, configure o id da entrega ou inclua um campo que mude a cada aviso (um número de sequência, a hora do evento).
Só registrar ou acionar o agente
Cada webhook tem um modo, que você troca no detalhe dele.
| Só registrar | Acionar o agente | |
|---|---|---|
| O que acontece com o aviso | Aparece na lista Últimos avisos da Atividade, que mostra os mais recentes (não é um histórico completo). | Além disso, o agente age sozinho a cada aviso válido, sem esperar por ninguém. |
| Gasta crédito | Não. | Sim, a cada aviso. |
| A resposta do agente | Não existe: não há agente. | Fica na conversa, dentro da plataforma. Se você quiser, também é enviada ao seu sistema: veja Receber a resposta do agente. |
| O agente precisa ser público | Não. | Sim. |
| Quando usar | Para testar a integração e ver o formato real dos avisos. | Quando o aviso já chega do jeito certo e o agente tem o que fazer com ele. |