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. Os seus webhooks nunca ficam atrás de um plano. São uma primitiva de desenvolvedor, como a própria API — e é disso que trata todo este guia até o final, onde estão os dois planos que a frase não cobre: a trilha de auditoria e os webhooks que você revende aos seus próprios clientes, que são um serviço que executamos continuamente em vez de um acesso ao seu próprio dado.

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. Editar ou apagar um post de feedback também não é entregue, e a razão vale ser dita porque não é sigilo: um post renomeado é público no instante em que é salvo. Nada reage a “um typo foi corrigido”, e entregabilidade é uma porta de mão única — ligar depois é uma capacidade nova, desligar é um contrato quebrado. Quando um fato é publicável e ninguém tem uso para ele, segurar é a única direção que continua reversível.

O plano de auditoria

Todo endpoint acima está no plano product: os fatos do seu produto, filtrados por subscribed_events. Existe um segundo plano, e cadastrar um endpoint com "plane": "audit" o coloca lá. Um endpoint de auditoria recebe a trilha de auditoria do staff — cada ação que alguém executou no painel, os mesmos registros que GET /v1/organization/audit-events mostra. Nenhum desses tipos está no catálogo acima e nenhum deles chega a um endpoint de produto: ler a trilha é audit:read, que só owner tem, enquanto webhooks:manage também está com admin — entregar esses fatos pelo fan-out comum tiraria um registro de acesso exclusivo do owner da organização sem que ninguém encostasse na permissão mais estreita. O plano de auditoria é a porta que tem essa permissão. Quatro coisas o diferenciam de um endpoint de produto, e todas seguem do fato de um registro ser um registro:
  • Ele leva o ator. Cada entrega tem um objeto actor a mais — user_id, email, ip, user_agent — que nenhuma entrega de produto tem, seja qual for o fato.
  • Ele recebe tudo. subscribed_events é recusado: um registro com as linhas interessantes filtradas se lê como se nada tivesse acontecido.
  • Ele é da organização, então é cadastrado no ambiente live e recebe as ações de staff dos dois ambientes. A trilha não é de um ambiente.
  • Ele exige audit:deliver — só do owner, e deliberadamente separada tanto de webhooks:manage quanto de audit:read: ler um registro e decidir para onde ele vai para sempre são atos diferentes. Cadastrar um também exige um plano que carregue a capacidade, e um plano que deixa de carregá-la interrompe as entregas. A trilha continua sendo gravada e guardada em todos os planos; o que um plano compra é o transporte.
O resto é idêntico: a mesma assinatura, os mesmos ids de evento, o mesmo contrato de ao-menos-uma-vez e sem ordem, o mesmo cronograma de retentativas e o mesmo log de entregas.

O plano do cliente: webhooks que você revende

Os dois planos acima entregam para você. O terceiro entrega para os seus clientes: um time seu cadastra um endpoint no portal e passa a receber os eventos que você registrou sobre aquele time — com os nomes do seu produto, não com os do catálogo acima. É a resposta para o pedido que todo SaaS B2B recebe cedo: “me avisa quando um pedido for aprovado, em vez de eu ficar consultando sua API”. Sem isso, você construiria fila, assinatura, retentativa, log e tela do zero. Com isso, seus clientes usam a mesma infraestrutura que você já usa.

Como funciona, de ponta a ponta

1. O seu backend reporta o fato, pela porta que já existe — nomeando o time a que ele pertence:
2. O seu cliente cadastra o endpoint dele, no portal, em /webhooks — ou pela API, com a sessão de contato dele:
3. A entrega chega no servidor dele, assinada como qualquer outra:

O que é diferente, e por quê

  • O vocabulário é o seu. O type é pedido.aprovado — o nome que você registrou. O catálogo do UserKit não chega aqui, e quem recebe nunca precisa saber que existe um UserKit no meio.
  • subscribed_events é obrigatório. No plano product, vazio significa todos; aqui não pode ser vazio. O seu vocabulário é aberto — não há lista fechada contra a qual verificar — então “todos” significaria assinar todo nome que você venha a inventar, para um receptor que não escreveu nenhum deles. São de 1 a 20 nomes, e no máximo 5 endpoints por time.
  • O escopo é o time, e um fato sem time não vai para ninguém. A entrega casa o customer_id do evento com o time dono do endpoint: um cliente nunca ouve o tráfego de outro. Uma linha de /v1/track sem customer_id é um fato da pessoa, não de um time — e não é entregue a nenhum endpoint deste plano. Se o evento importa para o cliente, nomeie o time.
  • Os seus próprios endpoints não recebem esse eco. O evento que você acabou de nos mandar não volta pelo plano product. Um eco é a primeira metade de um laço, e a sua stream de analytics já tem esse dado.
  • O aviso de falha vai para o cliente, não para você. Se o servidor dele parar de responder e o endpoint for desligado, quem recebe o e-mail é quem pode consertar — os administradores daquele time, na língua deles, assinado com a sua marca. Você não vira suporte de terceiro nível de máquinas que não são suas.
  • É do seu plano. Cadastrar exige um plano que carregue a capacidade (403 webhooks_not_available sem ele), e um downgrade interrompe as entregas sem apagar nada — os endpoints continuam cadastrados para quando o plano voltar. É a mesma posição do plano de auditoria, e pelo mesmo motivo: o que se vende aqui não é o acesso do cliente ao dado dele, é o transporte que executamos continuamente em nome dele.
Todo o resto é idêntico ao que você já leu: https:// obrigatório, HMAC sobre t.corpo, doze tentativas, desligamento após cinco entregas esgotadas, segredo reexibível e rotacionável, log de entregas e disparo de teste — tudo em /v1/contact/customer/webhooks/{id}/….

Quem administra, do lado do cliente

A capacidade é $webhooks.manage, do vocabulário de papéis do seu plano de clientes. O papel owner já vem com ela; se você definiu papéis próprios (um “financeiro”, um “técnico”), decida qual deles deve poder apontar os eventos do time para um servidor — é uma decisão sua, na tela de papéis, e não uma que tomamos por você.

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. O plano de auditoria acrescenta audit:deliver, só para owner — cadastrar, reapontar, desligar ou apagar um endpoint de auditoria. Ler um deles continua em webhooks:manage: perceber que existe uma exportação não é o mesmo ato que escolher para onde ela vai. O plano do cliente não usa nenhuma das duas. Ele é administrado pelo seu cliente, na sessão de contato dele, sob $webhooks.manage — uma capacidade do vocabulário de papéis do seu plano de clientes, não uma permissão de staff. A consequência prática: os endpoints dos seus clientes não aparecem na sua tela de webhooks e não são endereçáveis por /v1/organization/webhooks/{id} (responde 404). Eles são credenciais de outra parte, e o segredo de assinatura que a tela reexibe é dela, não sua.