Skip to main content
Em vez de consultar a API em laço, registre um endpoint de webhook e a Tryno entrega os eventos ao seu servidor. Cada entrega é assinada com HMAC-SHA256, então você consegue provar que a requisição veio da Tryno e não de outra pessoa.
Webhook é entrada de dados no seu sistema. Ele não é um canal de escrita na Tryno: a Platform API v1 é somente leitura, então o padrão é sempre evento chega → seu sistema reage → (opcional) você enriquece com GET /v1/…. Veja Receitas de integração.

Eventos disponíveis

Os três eventos de membro (member.joined/member.left/member.removed) têm um formato GARANTIDO. userId e handle (o handle da TRIBO, não do membro) estão sempre presentes, em todo caminho de disparo — incluindo entrada via convite aceito no cadastro. Todo data é montado por um único construtor no servidor (memberEventPayload), não por código duplicado em cada rota, então não existe uma origem do evento que omita handle.
Além desses, ping é entregue quando você dispara um teste. Ele não pode ser assinado na lista events do endpoint — é sempre enviado sob demanda, e seu data é { "message": "This is a test event from Tryno." }.
O payload é deliberadamente magro: ele carrega identificadores, não o registro inteiro. Se você precisa do nome do membro, do corpo do post ou do valor formatado, busque em /api/v1 com a sua chave depois de receber o evento. Isso mantém a entrega pequena e impede que dados sensíveis vazem por um endpoint mal configurado.

Registrar um endpoint

Formulário de registro de webhook na tela de administração da tribo, com o campo de URL e os eventos disponíveis para assinatura

Configuração de webhooks na aba API do admin da tribo — URL do endpoint e seleção dos eventos assinados.

Autenticado por sessão, como dono/administrador da tribo:
string
obrigatório
Precisa ser HTTPS e apontar para um host público.
string[]
obrigatório
Lista de eventos. Valores desconhecidos são descartados em silêncio; se sobrar zero evento válido, a resposta é 400 no_events. Duplicatas são removidas.
string
Texto livre de até 140 caracteres, só para você identificar o endpoint.
A resposta 201 traz o endpoint criado, incluindo o segredo de assinatura (whsec_ + 32 bytes aleatórios em base64url). Diferente das chaves de API, o segredo permanece visível na listagem — você precisa dele para verificar assinaturas.
A URL precisa ser HTTPS e resolver para um host público. Loopback (localhost, 127.x, ::1, 0.0.0.0), redes privadas (10.x, 192.168.x, 172.16–31.x), link-local (169.254.x, fe80::), ULA (fc00::/7) e os sufixos .local e .internal são recusados com invalid_url. É uma proteção contra SSRF: um endpoint não pode apontar uma requisição assinada para dentro de uma infraestrutura.
Desenvolvendo local? Como localhost é recusado, use um túnel HTTPS público (ngrok, Cloudflare Tunnel, localtunnel) e registre a URL do túnel. Depois, dispare o ping para validar a verificação de assinatura antes de esperar tráfego real.

Formato da entrega

POST no seu endpoint, com content-type: application/json e corpo:
string
Id da entrega. É o mesmo valor do cabeçalho x-tryno-delivery — a chave de deduplicação.
string
Nome do evento, igual ao cabeçalho x-tryno-event.
integer
Unix em segundos do momento em que o evento foi enfileirado — não do envio. Numa reentrega após 6 horas, created continua sendo o instante original.
object
Os campos do evento (tabela acima). Trate todos como opcionais.
E os cabeçalhos:
O t da assinatura é o instante do envio, e muda a cada tentativa. O created do corpo é o instante do evento e não muda. Sua janela de replay se aplica ao t, nunca ao created — senão toda reentrega legítima seria rejeitada.

Verificar a assinatura

Este é o único passo que você não pode pular. Um endpoint que aceita qualquer POST é um endpoint que qualquer pessoa na internet pode usar para injetar eventos falsos no seu CRM. A assinatura é HMAC-SHA256 do segredo do endpoint sobre a string concatenada "<t>" + "." + "<corpo bruto>", em hexadecimal minúsculo.
1

Leia o corpo BRUTO, antes de qualquer parse

Bytes exatamente como chegaram. Ver o aviso abaixo.
2

Quebre o cabeçalho x-tryno-signature

Separe por vírgula, depois por =, e extraia t e v1. Se faltar qualquer um dos dois, rejeite.
3

Confira a janela de replay

Rejeite se |agora − t| for maior que 300 segundos. Sem isso, um atacante que capturou uma entrega antiga pode reenviá-la para sempre.
4

Recalcule e compare em tempo constante

HMAC_SHA256(segredo, t + "." + corpoBruto) em hex, comparado com v1 usando uma função de comparação em tempo constante — nunca ==.
5

Responda 2xx rápido, processe depois

Qualquer coisa fora de 2xx, ou acima de 10s, conta como falha e vira reentrega.
O corpo bruto é obrigatório. A maioria dos frameworks faz JSON.parse do corpo antes do seu handler. Se você re-serializar o objeto para calcular o HMAC, a assinatura não vai bater — a ordem das chaves, os espaços e o escape de Unicode mudam os bytes. Assine sobre os bytes originais.
  • Expressexpress.raw({ type: "application/json" }) nessa rota específica (antes de qualquer express.json() global), e use req.body como Buffer.
  • Fastify — registre um addContentTypeParser("application/json", { parseAs: "buffer" }, …) que guarde o buffer.
  • Next.js (App Router) / Hono / Workersawait request.text(). Chame uma vez e reaproveite a string; o corpo não pode ser lido duas vezes.
  • Flaskrequest.get_data() (o request.json já consumiu o parse, mas o Flask mantém os bytes).
  • Djangorequest.body.
  • FastAPI — declare o handler com request: Request e use await request.body(), em vez de um modelo Pydantic no parâmetro.
  • Laravel$request->getContent().
  • Go (net/http)io.ReadAll(r.Body) e faça o parse depois, a partir desses mesmos bytes.
A assinatura recebida é pública, mas a esperada é derivada do seu segredo. Um == comum retorna assim que encontra o primeiro byte diferente, e essa diferença de tempo é mensurável em rede. Com tentativas suficientes, um atacante consegue descobrir a assinatura correta byte a byte sem nunca ver o segredo. timingSafeEqual / hmac.compare_digest / hmac.Equal sempre percorrem o buffer inteiro.

Novas tentativas

Sua resposta é considerada bem-sucedida com qualquer status 2xx. Qualquer outra coisa — incluindo 3xx, que não é seguido — ou um tempo de resposta acima de 10 segundos conta como falha, e a Tryno tenta de novo com espera crescente: Depois da sexta tentativa a entrega é marcada como failed e não é mais tentada. São, no total, até ~8h40 de janela de recuperação — folga de sobra para um deploy ou uma indisponibilidade curta do seu lado.
As entregas são drenadas por um cron a cada 5 minutos, em lotes de até 50 por ciclo. Ou seja: a entrega é quase em tempo real, não instantânea, e a “espera de +1 minuto” na prática arredonda para o próximo ciclo. Se o seu caso exige latência de segundos, consulte a API em vez de esperar o evento.
Como o lote é de 50 por ciclo, uma tribo que gere uma rajada muito grande de eventos (por exemplo, um lançamento com centenas de compras em minutos) drena a fila ao longo de vários ciclos. Isso é vazão, não perda: nada é descartado.

Idempotência: obrigatória, não opcional

A v1 não usa trava de linha na fila de entregas. Dois ciclos de cron sobrepostos podem, em tese, entregar a mesma linha duas vezes — e reentregas por timeout também acontecem quando o seu servidor processou o evento mas não conseguiu responder a tempo. Deduplique por x-tryno-delivery (idêntico ao id do corpo). É exatamente para isso que ele existe: o valor é estável entre todas as tentativas da mesma entrega.
Guardando o id da entrega antes de processar
Responder 2xx a uma duplicata é o comportamento correto. Se você responder erro para “já processei isso”, a Tryno vai continuar reentregando por até seis tentativas o evento que você de fato já tratou.
Se o seu destino final já é idempotente por natureza (um UPSERT por userId, por exemplo), você pode dispensar a tabela de log. O que não dá é ter efeitos colaterais não idempotentes — enviar e-mail, cobrar, postar em canal — sem nenhuma proteção.

Testar e depurar

O log de entregas devolve, por linha: id, event, status (pending / success / failed), attempts, responseStatus (o código HTTP que o seu servidor devolveu, ou null se a conexão nem chegou a completar), lastError, nextAttemptAt, deliveredAt e createdAt.
  • responseStatus: 401 repetido — sua verificação de assinatura está rejeitando. Quase sempre é corpo re-serializado (veja o aviso do corpo bruto) ou o segredo errado.
  • responseStatus: null com lastError de timeout — seu handler demorou mais de 10s. Responda 2xx primeiro e processe em fila.
  • responseStatus: 404 / 405 — a URL registrada não é a rota que aceita POST.
  • status: pending sem avançar — o endpoint está desativado (enabled: false). Veja a nota abaixo.
  • responseStatus: 3xx — redirecionamentos não são seguidos. Registre a URL final.
Um endpoint desativado (enabled: false) não recebe entregas, e as que já estavam na fila para ele ficam pending até que você o reative — elas não expiram sozinhas. Se a intenção é desligar de vez, apague o endpoint em vez de desativá-lo: a exclusão remove também as entregas pendentes.

Gerenciar

O PATCH é parcial: envie só os campos que mudam. Um corpo sem nenhum campo reconhecido responde 400 empty. Ao trocar events, o valor é substituído por inteiro, não somado — envie a lista completa que você quer manter.
Não há rotação do segredo de assinatura. Não existe endpoint para gerar um novo whsec_ num endpoint existente, nem suporte a duas assinaturas simultâneas. Se o segredo vazar, o caminho é: registrar um novo endpoint (com novo segredo), passar a aceitar os dois no seu receptor, e então excluir o antigo. Dizemos isso em vez de descrever uma rotação que o produto não tem.

Boas práticas de recepção

  • Responda em menos de 10s. Valide a assinatura, grave o evento numa fila e responda 204. Todo o resto é trabalho de segundo plano.
  • Trate data como extensível. Campos novos podem aparecer numa versão futura; os campos já documentados na tabela acima, porém, são garantidos — não variam por origem do evento (isso vale inclusive para member.joined).
  • Não confie na ordem. Um order.paid pode chegar antes de um member.joined do mesmo usuário — o backoff reordena entregas com falha.
  • Um endpoint por consumidor. Assim você desativa um destino sem afetar os outros, e o log de entregas fica legível.
  • Guarde o whsec_ como segredo. Ele é equivalente a uma senha: quem o tem consegue forjar eventos para o seu servidor.

Próximo passo — receitas de integração →

Monte o fluxo completo: evento chega, você enriquece com /v1 e empurra para planilha, Zapier ou n8n.