Todos os artigos
Como testar webhooks do Zoom localmente com HTTPS
ZoomwebhookslocalhostHMAC verification

Como testar webhooks do Zoom localmente com HTTPS

Para testar webhooks do Zoom no localhost, exponha sua rota POST local com npx portpreview PORT, informe essa rota HTTPS pública como endpoint de notificação de eventos e implemente o desafio endpoint.url_validation do Zoom antes de clicar em Validate. Nos eventos normais, verifique x-zm-signature usando o corpo intacto da requisição e o timestamp, persista o evento de forma idempotente e retorne 2xx em até três segundos.

Como as assinaturas de eventos do Zoom chegam ao localhost

Os webhooks do Zoom são notificações HTTP POST em JSON para eventos assinados em produtos como Meetings, Webinars, Phone, Team Chat, Rooms e outros serviços disponíveis para seu aplicativo. O catálogo exato de eventos e seus campos depende do tipo de aplicativo, dos produtos habilitados, das permissões da conta, dos escopos e da plataforma atual do Zoom. Selecione apenas os eventos que seu handler entende e use o schema atual exibido no fluxo de criação do aplicativo.

Seu endpoint precisa estar acessível publicamente por HTTPS, com nome de domínio totalmente qualificado, uma cadeia válida de certificados emitidos por uma autoridade certificadora, TLS 1.2 ou posterior e suporte a requisições POST com JSON. Uma URL de loopback como http://localhost:3000 não atende a esses requisitos. O PortPreview fornece a entrada HTTPS pública e encaminha as requisições para seu processo local.

A documentação oficial de webhooks do Zoom é a fonte definitiva sobre requisitos do endpoint, validação por desafio e resposta, assinaturas de eventos, comportamento de entrega e etapas atuais de configuração.

Crie uma rota Express que preserve o corpo bruto

A assinatura de requisição do Zoom cobre o texto exato do corpo. Capture os bytes antes que qualquer middleware JSON faça o parsing e a serialização. O exemplo abaixo trata a validação e a verificação de eventos normais em uma única rota:

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.post('/webhooks/zoom', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  const rawBody = req.body.toString('utf8');
  let event;
  try {
    event = JSON.parse(rawBody);
  } catch {
    return res.status(400).json({ error: 'Invalid JSON' });
  }

  const secret = process.env.ZOOM_WEBHOOK_SECRET_TOKEN;
  if (!secret) return res.sendStatus(500);

  if (event.event === 'endpoint.url_validation') {
    const plainToken = event.payload?.plainToken;
    if (typeof plainToken !== 'string') return res.sendStatus(400);
    const encryptedToken = crypto
      .createHmac('sha256', secret)
      .update(plainToken)
      .digest('hex');
    return res.status(200).json({ plainToken, encryptedToken });
  }

  const timestamp = req.get('x-zm-request-timestamp') ?? '';
  const received = req.get('x-zm-signature') ?? '';
  const message = `v0:${timestamp}:${rawBody}`;
  const expected = `v0=${crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex')}`;

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  if (!valid) return res.sendStatus(401);

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return res.sendStatus(401);

  const requestId = req.get('x-zm-request-id');
  const deliveryKey = requestId || crypto.createHash('sha256').update(rawBody).digest('hex');
  await saveWebhookOnce({ provider: 'zoom', deliveryKey, event });
  return res.sendStatus(200);
});

app.listen(3000);

A janela de cinco minutos para considerar a requisição recente é uma política de segurança da aplicação neste exemplo, não um substituto para a verificação HMAC. Escolha uma tolerância compatível com a sincronização dos seus relógios e com as expectativas de entrega. Registre apenas a categoria do motivo das falhas, nunca o token secreto nem o corpo completo da requisição.

Inicie o túnel local

  1. Execute a aplicação e confirme que a rota aceita um POST local na porta 3000.
  2. Abra outro terminal e execute npx portpreview 3000. Substitua 3000 pela porta realmente usada por sua aplicação.
  3. Acrescente a rota à origem gerada, por exemplo, https://YOUR-TUNNEL.portpreview.dev/webhooks/zoom.
  4. Mantenha a aplicação e o túnel em execução durante a validação e os testes de eventos.

Quando um novo túnel tem outro hostname, o Zoom o considera um endpoint diferente. Atualize e valide a nova URL antes de esperar eventos. A URL deve apontar diretamente para o handler POST; redirecionamentos não são adequados para uma entrega confiável de webhooks, e o Zoom não tenta novamente respostas 3xx.

Adicione a assinatura de eventos no Zoom

No Zoom App Marketplace, abra o aplicativo criado e acesse a área Features ou Access indicada pelo fluxo atual de criação. Habilite Event Subscriptions, adicione uma assinatura, escolha os tipos de evento e o receiver e cole a URL HTTPS completa do endpoint. As opções de receiver e eventos disponíveis variam conforme o tipo de aplicativo e a configuração da conta. Aplicativos publicados podem precisar passar por uma nova análise quando as assinaturas mudam.

Copie o webhook secret token associado ao aplicativo para uma variável de ambiente local ignorada pelo controle de versão, como ZOOM_WEBHOOK_SECRET_TOKEN. Ele não é um OAuth client secret, access token nem o verification token obsoleto. Reinicie o servidor local depois de alterar o ambiente.

Implemente corretamente a validação da URL do endpoint

Ao clicar em Validate, o Zoom envia um POST cujo event é endpoint.url_validation. O payload contém plainToken. Calcule um HMAC SHA-256 usando o webhook secret token como chave e esse plain token como mensagem, codifique o resumo em hexadecimal minúsculo e responda com um JSON que contenha tanto o plainToken inalterado quanto o encryptedToken resultante.

const encryptedToken = createHmac('sha256', webhookSecret)
  .update(event.payload.plainToken)
  .digest('hex');

return {
  plainToken: event.payload.plainToken,
  encryptedToken
};

Responda com HTTP 200 e o corpo JSON em até três segundos. Não gere o hash da requisição de validação inteira, não use o OAuth client secret, não codifique o resumo em Base64 e não adicione o prefixo v0= ao resumo da validação. Essas opções pertencem a outros fluxos. O endpoint não pode ser salvo até que a validação inicial seja concluída.

A documentação atual do Zoom também descreve uma revalidação automática a cada 72 horas. Após falhas repetidas de revalidação, o proprietário do aplicativo recebe notificações; depois de seis falhas consecutivas, o Zoom desabilita a assinatura de eventos e interrompe os eventos. Portanto, um túnel de desenvolvimento que foi fechado falhará em uma validação posterior. Remova assinaturas temporárias depois dos testes e mantenha o tratamento do desafio permanentemente disponível em produção.

Verifique as requisições normais de webhook do Zoom

A validação da URL prova que o endpoint conhecia o segredo no momento do desafio. Separadamente, a verificação de eventos normais prova que o corpo recebido corresponde ao HMAC enviado pelo Zoom. Leia x-zm-request-timestamp e monte exatamente esta mensagem:

v0:{x-zm-request-timestamp}:{raw request body}

Calcule o HMAC SHA-256 dessa mensagem usando o webhook secret token como chave, codifique o resumo em hexadecimal, adicione v0= no início e compare o resultado com x-zm-signature usando uma comparação em tempo constante. O corpo precisa ser o corpo original da requisição. Fazer o parsing do JSON e depois chamar JSON.stringify pode alterar espaços ou a formatação das propriedades e invalidar uma assinatura legítima.

Rejeite assinaturas ausentes, malformadas, inválidas ou antigas demais antes de executar a lógica de negócio. Mantenha o relógio do sistema sincronizado. O antigo webhook verification token foi descontinuado e tinha encerramento previsto para junho de 2025; códigos novos devem usar o fluxo HMAC com secret token documentado pelo Zoom, e não uma comparação do header Authorization copiada de um tutorial antigo. Consulte o guia de verificação de assinatura de webhooks para detalhes sobre corpo bruto e comparação resistente a ataques de temporização.

Distribua os tipos de evento específicos do provedor

Mesmo um payload verificado ainda precisa de validação de schema e verificações de autorização. Em eventos de reunião, identificadores como meeting ID e UUID têm finalidades diferentes; reuniões repetidas ou recorrentes tornam importante a correlação por UUID. Trate os campos do payload conforme cada evento e siga a referência atual de cada assinatura.

async function processZoomEvent(event) {
  switch (event.event) {
    case 'meeting.started':
      await markMeetingStarted({
        uuid: event.payload.object.uuid,
        startedAt: event.payload.object.start_time
      });
      break;
    case 'meeting.ended':
      await markMeetingEnded({
        uuid: event.payload.object.uuid,
        endedAt: event.payload.object.end_time
      });
      break;
    default:
      await recordUnhandledZoomEvent(event.event);
  }
}

Não presuma que a ordem dos eventos seja um log de transações. Atrasos de rede, retentativas e processamento paralelo podem produzir uma ordem inesperada de chegada. Armazene o timestamp do evento fornecido pelo provedor e aplique regras monotônicas de estado quando necessário. Tipos de evento desconhecidos devem ser observáveis e receber confirmação após uma persistência segura, em vez de causarem falhas repetidas no endpoint.

Cumpra o prazo de entrega de três segundos

O Zoom espera HTTP 200 ou 204 em até três segundos para considerar a entrega bem-sucedida. Verifique a requisição, valide o envelope mínimo, grave em uma caixa de entrada durável ou fila e responda. Processamento de vídeo, atualizações no CRM, chamadas de calendário, e-mail e analytics devem ser executados por workers.

Segundo a documentação atual do Zoom sobre entrega de notificações, falhas elegíveis de servidor e conexão são tentadas novamente três vezes: aproximadamente cinco minutos após a tentativa inicial, 20 minutos depois dessa retentativa e 60 minutos depois da segunda retentativa. O Zoom considera 2xx um sucesso; não tenta novamente redirecionamentos 3xx nem erros de cliente 4xx. Como essas políticas podem mudar, confira novamente a página oficial antes de criar alertas operacionais baseados em intervalos exatos.

Torne todos os eventos idempotentes

Uma retentativa pode ocorrer depois de um timeout ambíguo, mesmo que sua primeira tentativa tenha sido confirmada. Elimine duplicidades antes dos efeitos colaterais. O header x-zm-request-id aparece na estrutura de requisição documentada pelo Zoom, mas o código deve tolerar sua ausência quando produtos ou versões forem diferentes. Use-o quando estiver presente; caso contrário, derive uma chave estável de dados imutáveis e verificados do evento ou de um resumo criptográfico do corpo bruto verificado. Garanta a unicidade no armazenamento, e não apenas em um cache em memória.

await db.transaction(async (tx) => {
  const claimed = await tx.webhookInbox.insertOnce({
    provider: 'zoom',
    deliveryKey,
    eventType: event.event,
    payload: event
  });
  if (!claimed) return;
  await tx.jobs.enqueue({ type: 'process-zoom-event', deliveryKey });
});

Mantenha atômicas a reserva da entrada na inbox e a criação do job. Se um worker falhar, tente o job novamente sem pedir que o Zoom refaça a entrega. O guia de retentativas e idempotência de webhooks aborda tabelas de inbox, chaves únicas e limites de efeitos colaterais.

Solucione problemas de validação e entrega

Validate informa uma falha

Confirme que a URL usa HTTPS público, inclui a rota exata, não tem redirecionamento e alcança a porta local ativa. Verifique se a resposta é um JSON HTTP 200 com o plain token original e o HMAC hexadecimal em minúsculas calculado apenas sobre esse token. Meça o tempo total de resposta; a validação precisa terminar em até três segundos.

Todos os eventos normais falham na verificação de assinatura

Confirme que você copiou o webhook secret token, e não um OAuth client secret ou o antigo verification token. Capture o corpo como bytes brutos antes do middleware JSON, use o header de timestamp exato, inclua os dois-pontos em v0:timestamp:body e adicione o prefixo v0= apenas à assinatura final do evento.

A validação funciona, mas os eventos não chegam

Confira se a assinatura está habilitada e salva, se os tipos de evento e o receiver pretendidos estão selecionados e se sua conta ou seus usuários geram esses eventos. Verifique o status da revalidação e os requisitos de publicação do aplicativo. Confirme que a URL do túnel não mudou desde a validação.

O handler funciona, mas o Zoom tenta novamente

Inspecione a latência e o status da resposta pública, não apenas os logs locais. Um trabalho lento pode ultrapassar três segundos mesmo que termine depois. Persista rapidamente, retorne 2xx e processe de forma assíncrona. Uma inbox protegida contra duplicidades impede que a retentativa repita o efeito colateral.

Um evento capturado falha quando é reproduzido mais tarde

Uma verificação de validade temporal deve rejeitar um timestamp antigo, e alterar o JSON invalida o HMAC. Em testes de ponta a ponta, gere um novo evento do provedor. Em testes de lógica de negócio, armazene uma fixture analisada e sem dados sensíveis e chame o dispatcher depois de ignorar a verificação de entrada somente no ambiente de testes. O guia de reprodução e depuração de webhooks explica essa separação.

Checklist de segurança para testar webhooks do Zoom

  • Mantenha os tokens secretos de webhook em arquivos de ambiente ignorados pelo controle de versão e rotacione credenciais expostas.
  • Verifique o HMAC sobre o corpo bruto antes de confiar em qualquer campo do payload.
  • Aplique uma tolerância de timestamp e sincronize o relógio do servidor para reduzir o risco de replay.
  • Valide o tipo de evento, o contexto da conta, os identificadores de objetos, o tipo de conteúdo e o tamanho do corpo.
  • Remova dos logs nomes de participantes, endereços de e-mail, assuntos de reuniões, conteúdo de chats e dados de gravações.
  • Use endpoints ou segredos separados para desenvolvimento e produção quando a configuração do aplicativo permitir.
  • Remova URLs públicas e assinaturas temporárias quando a sessão local terminar.

O padrão pronto para produção é o mesmo comprovado localmente: entrada HTTPS estável, tratamento permanente do desafio, verificação HMAC do corpo bruto, idempotência durável, confirmação em menos de três segundos e workers isolados. Para rastreamento geral de requisições e verificação de rotas, siga o guia de depuração local de webhooks.

Perguntas frequentes

Como validar uma URL de webhook do Zoom no localhost?
Exponha a rota local por HTTPS e responda a endpoint.url_validation retornando o plainToken original e seu encryptedToken em HMAC SHA-256 hexadecimal, calculado com o webhook secret token do Zoom.
Como verificar a assinatura de um webhook normal do Zoom?
Monte v0:{x-zm-request-timestamp}:{raw body}, calcule o HMAC SHA-256 com o webhook secret token, adicione v0= ao início do resumo hexadecimal e compare o resultado com x-zm-signature.
Por que a validação da URL do Zoom funciona, mas a verificação do evento falha?
Os fluxos calculam o hash de mensagens diferentes. A validação usa apenas plainToken, enquanto eventos normais assinam uma versão, o timestamp da requisição e o corpo bruto exato. Fazer o parsing e serializar novamente o corpo pode invalidar a verificação.
Em quanto tempo um webhook do Zoom precisa responder?
A documentação atual do Zoom exige HTTP 200 ou 204 em até três segundos. Persista ou enfileire de forma durável o evento verificado, responda e execute a lógica de negócio demorada de forma assíncrona.