Skip to main content
Um endpoint é uma URL, um segredo e uma lista de tipos de evento. Todo fato que o UserKit publica internamente pode sair por ele, e é por isso que o recurso foi barato de construir e que nada nos eventos muda de formato na borda. Webhooks nunca ficam atrás de um plano. São uma primitiva de desenvolvedor, como a própria API.

Registrar um endpoint

Três coisas dessa chamada merecem ser ditas em voz alta. https://. As entregas carregam fatos sobre os seus próprios clientes e uma assinatura que prova quem enviou, e nenhum dos dois sobrevive a ser lido no fio. Um redirect é recusado em vez de seguido — registre o endereço que de fato responde. Um endpoint pertence a um ambiente e nunca sai dele: um endpoint de teste ouve tráfego de teste, um de produção ouve tráfego de produção. Fatos sobre a própria organização (organization.created, member.invited) não carregam ambiente e vão para os endpoints live — uma organização tem uma existência real só, e o ambiente de teste é onde o plano de clientes é ensaiado, não onde a organização está. subscribed_events vazio significa todos. Um tipo adicionado ao catálogo depois passa a chegar sem você mexer em nada, o que costuma ser o que se quer no primeiro endpoint e raramente é o que se quer no terceiro.

O payload

data carrega ids, não fotografias — releia o recurso para saber o estado atual. Isso mantém nomes e endereços fora de toda fila, log e buffer de retry entre nós e você, e significa que um handler atrasado lê o que é verdade agora em vez do que era verdade quando o evento foi escrito.

A entrega é ao menos uma vez, e fora de ordem

Isso é o contrato, não uma ressalva. A mesma entrega pode chegar duas vezes e a ordem de chegada não é a ordem dos fatos. Dois campos existem para tornar isso tratável, e usá-los não é opcional:
  • id é por onde você deduplica. Ele é estável em todo retry e em um reenvio manual — um reenvio é o mesmo fato chegando de novo, e um handler que já processou precisa poder dizer isso. Anote o id antes de agir sobre o evento; ignore o que já viu.
  • sequence é por onde você ordena. É um número global monotônico atribuído no commit: de dois eventos sobre o mesmo contato, o maior é o fato mais recente. Compare-o. Não compare horários de chegada, e não assuma que contact.identified chega antes de contact.signed_in.
Os headers carregam os mesmos dois valores, e mais um: UserKit-Delivery-Id nomeia este conjunto de tentativas. Um retry mantém o mesmo, um reenvio ganha outro. É o que se cita numa conversa de suporte; UserKit-Event-Id é o que o seu código usa para decidir.

Verificando a assinatura

Toda requisição carrega:
v1 é HMAC-SHA256(segredo, "<t>.<corpo cru>"), em hex. Assine os bytes crus que você recebeu — fazer parse e serializar de novo muda os bytes.
O timestamp está dentro do que é assinado, e conferi-lo é a sua metade do acordo. Assinar só o corpo tornaria uma requisição capturada válida para sempre: quem gravasse uma entrega poderia reenviá-la a qualquer momento e a assinatura continuaria batendo. Assinados juntos — e recusados quando velhos — você tem uma janela de cinco minutos em vez de um buraco aberto. Compare em tempo constante, e trate v1 como uma etiqueta em vez de uma suposição: leia os pares que você conhece e ignore o resto, para que um algoritmo futuro possa ser adicionado ao lado deste em vez de no lugar dele.

O segredo pode ser exibido de novo

Diferente de uma API key, você pode lê-lo outra vez em GET /v1/organization/webhooks/{id}/secret. Isso é uma consequência, não uma conveniência. Uma API key só é comparada, então guardar o hash basta; um segredo de webhook precisa assinar cada entrega, então a linha guarda o segredo de qualquer jeito. Uma vez que isso é verdade, mostrá-lo uma única vez só impediria você de ler o que o banco contém às claras — ao custo da única recuperação que não é “rotacionar e quebrar todos os verificadores em produção de uma vez”. A rotação é o controle de verdade, e ela não tem janela de tolerância: o novo segredo vale a partir da próxima entrega. Dois segredos vivos significariam um verificador que aceita qualquer um dos dois, que é exatamente a propriedade que a rotação existe para remover. Publique o segredo novo primeiro, rotacione depois.

Retentativas, e quando paramos

O seu endpoint tem dez segundos para responder. Qualquer 2xx é sucesso; confirme primeiro e faça o trabalho depois. Uma tentativa que falha é retentada doze vezes, dobrando a partir de 30 segundos e com teto de seis horas:
São cerca de quatorze horas e meia da primeira tentativa até a última — tempo suficiente para que um deploy ruim descoberto de manhã não tenha perdido nada, e curto o bastante para que um endereço permanentemente morto pare de ser chamado dentro de um dia. Cada espera carrega uma pitada de jitter, que só subtrai: sem ele, uma rajada de entregas retentaria em sincronia e chegaria num servidor em recuperação como a mesma rajada que o derrubou. O retry é uma coluna numa tabela, não um timer num processo — um deploy no meio do cronograma não muda nada.

Desligamento automático

Cinco entregas seguidas esgotando todas as tentativas desligam o endpoint. Isso leva pelo menos as quatorze horas que a primeira delas passou retentando, sobre cinco fatos diferentes, que é o que “sustentado” precisa significar antes de pararmos de chamar o seu servidor. Um único sucesso zera a sequência. Um endpoint desligado não recebe nada novo na fila — e os owners da sua organização recebem um e-mail, porque uma integração que fica quieta sem avisar é descoberta pela ausência. Nada se perde: as entregas estão no histórico, e reenviar uma é um clique assim que o endereço voltar. Religar zera a sequência, então a primeira falha depois do conserto não desliga tudo de novo.

O histórico, o reenvio e o disparo de teste

GET /v1/organization/webhooks/{id}/deliveries são as últimas cem entregas, mais recentes primeiro, com as tentativas gastas, o status que voltou e quanto tempo levou. Entregas concluídas ficam 30 dias; uma pendente nunca é removida. POST …/deliveries/{deliveryId}/replay envia o corpo guardado de novo, byte a byte, com um novo id de entrega e o mesmo id de evento. POST …/test enfileira uma entrega com o tipo webhook.test — deliberadamente ausente do catálogo, para que um handler que reage a ele esteja reagindo a um botão que alguém apertou, não a um fato. Os dois respondem 202: a linha é durável, a tentativa ainda não aconteceu, e o resultado aparece no histórico.

O catálogo publicado

Este é o contrato contra o qual você constrói, e é a mesma lista que o fan-out da entrega lê — um tipo ausente daqui é um tipo que não vai chegar. GET /v1/organization/webhooks/events devolve o catálogo em tempo de execução. Um fato é publicado internamente e nunca entregue: webhook.endpoint_disabled. Ele seria endereçado ao endpoint que acabou de parar de responder, o que é uma mensagem para ninguém — o e-mail para os owners é o que carrega esse recado.

Permissão

Tudo acima exige webhooks:manage, concedida a owner e admin por padrão. Não há divisão entre leitura e escrita, porque a tela mostra o segredo de assinatura: ler um endpoint é possuir a credencial dele.