Webhooks de Assinatura em Fila: Ack Rápido, Reprocessamento e Reconciliação
Todo mundo integra o webhook de assinatura do mesmo jeito no primeiro dia: um endpoint que recebe o evento, grava no banco, avisa o ERP e responde. Funciona no sandbox e quebra no primeiro pico de produção, quando o banco demora, o ERP está fora e a entrega expira. Este guia trata o webhook como o que ele é, uma mensagem que pode chegar atrasada, repetida ou fora de ordem, e mostra o desenho que sobrevive a isso: ack rápido, fila, deduplicação por id, reprocessamento e reconciliação com a API quando algo escapar.
Os fatos abaixo sobre a política de entrega da SignDocs vêm da implementação da plataforma, não de suposições. O catálogo de eventos e o formato do payload estão em webhooks e eventos da API; aqui o assunto é o que o seu lado precisa fazer com eles.
O que a SignDocs promete (e o que não promete) na entrega
Cada evento é uma requisição POST para a URL registrada, com o corpo em JSON e dois cabeçalhos: X-SignDocs-Signature (HMAC-SHA256 em hexadecimal, calculado sobre {timestamp}.{payload}) e X-SignDocs-Timestamp (segundos Unix). A entrega é feita por um worker alimentado por fila, e a política é esta:
| Aspecto | Comportamento | Consequência para o seu consumidor |
|---|---|---|
| Timeout por tentativa | 15 segundos | Responder em milissegundos; nunca esperar o banco ou o ERP |
| Retentativas | Até 3 tentativas no total para erros de rede e respostas 5xx, com espera curta e crescente entre elas | Uma indisponibilidade de alguns segundos é absorvida; uma de minutos, não |
| Depois da última tentativa | A entrega é registrada como FAILED e não é repetida indefinidamente | Você precisa de reconciliação própria para os casos perdidos |
| Resposta 4xx | Não é retentada: é tratada como recusa do seu endpoint | Um bug de validação seu descarta o evento |
| Ordem | Não garantida entre eventos da mesma transação | Consumidor tolerante a chegada fora de ordem |
| Duplicidade | Possível (retentativa após um 2xx que se perdeu na rede) | Deduplicar pelo id do evento |
Isso é o contrato honesto de qualquer webhook: entrega ao menos uma vez, com esforço limitado. Quem promete "exatamente uma vez, sempre" está omitindo alguma parte. O desenho do consumidor precisa partir daí.
Regra 1: responder 2xx em menos de dois segundos
O endpoint que recebe o webhook tem uma única tarefa: verificar que a mensagem é autêntica e guardá-la em um lugar durável. Só isso. Todo o resto (atualizar o contrato, baixar o PDF assinado, chamar o CRM) acontece depois, fora da requisição.
Repare que o corpo vai para a fila já verificado e no formato bruto. Verificar antes de enfileirar evita que um atacante encha a sua fila; guardar o bruto permite auditar mais tarde exatamente o que foi recebido.
Três filas, o mesmo padrão
- SQS (AWS): use uma fila FIFO com
MessageDeduplicationId = evento.ideMessageGroupId = transactionId. A deduplicação nativa vale por 5 minutos; o grupo mantém a ordem por transação dentro do que a SignDocs enviou. Configure uma dead-letter queue commaxReceiveCountde 5 para o que o worker não conseguir processar. - BullMQ (Node/Redis):
queue.add(evento.eventType, evento, { jobId: evento.id, attempts: 5, backoff: { type: 'exponential', delay: 2000 } }). UmjobIdrepetido é ignorado, o que resolve a duplicidade sem código extra. - Sidekiq (Ruby):
SignDocsEventJob.perform_async(evento)com o gemsidekiq-unique-jobsusando oiddo evento como chave de unicidade, ou uma tabelawebhook_eventscom índice único emevent_idcomo barreira antes doperform_async.
A fila não substitui a tabela. Além de enfileirar, grave o evento bruto em uma tabela webhook_events(event_id UNIQUE, event_type, transaction_id, received_at, raw, processed_at). O UNIQUE é a deduplicação definitiva (a do SQS expira em 5 minutos); o processed_at nulo é a sua lista de pendências para reprocessar.
Regra 2: o worker é idempotente e tolera desordem
O worker consome a fila e aplica o evento ao seu domínio. Duas propriedades o tornam seguro:
Idempotência. Aplicar o mesmo evento duas vezes não pode ter efeito duplo. "Marcar contrato como assinado" é naturalmente idempotente; "enviar e-mail de boas-vindas" não é. Para as ações não idempotentes, verifique processed_at na tabela antes de agir e marque-o na mesma transação de banco em que a ação é registrada.
Tolerância a desordem. Um STEP.COMPLETED pode chegar depois do SIGNING_SESSION.COMPLETED da mesma sessão. Modele o estado do seu contrato como uma máquina que só avança: receber um evento "menor" quando o estado já é "maior" é um no-op, nunca uma regressão.
O download do PDF assinado e do pacote de evidências (GET /v1/transactions/{id}/download e /evidence) vai para outra fila, com seus próprios retries: são chamadas de rede que podem falhar por motivos que nada têm a ver com o evento.
Regra 3: reprocessar é normal, não exceção
Com a tabela de eventos, reprocessar é trivial: selecione tudo com processed_at IS NULL mais antigo que alguns minutos e reenfileire. Isso cobre o worker que caiu no meio, o deploy que reiniciou o processo e o bug que lançou exceção. Como o worker é idempotente, reprocessar algo que na verdade já foi aplicado não causa dano.
Para testar esse caminho antes de precisar dele, use POST /v1/webhooks/{webhookId}/test: a plataforma envia um evento de teste real, assinado com o segredo daquele webhook, para a URL registrada. Ele exercita o HMAC, a fila, o worker e a tabela de ponta a ponta. Coloque essa chamada no seu pipeline de CI, como descrito em testar a integração no sandbox e no CI, e derrube o worker de propósito uma vez para ver o reprocessamento funcionando.
Regra 4: reconciliar com a API o que o webhook não trouxe
Mesmo com tudo acima, existe o caso em que a SignDocs tentou três vezes, o seu endpoint estava fora nas três e a entrega ficou como FAILED. O evento nunca chegou. A resposta não é implorar por retentativas infinitas; é uma rotina de reconciliação que pergunta à fonte da verdade.
Os status possíveis da transação (CREATED, DOCUMENT_UPLOADED, IN_PROGRESS, COMPLETED, CANCELLED, EXPIRED, FAILED) mapeiam para os mesmos estados do seu domínio; GET /v1/transactions/{id} também devolve o metadata que você enviou na criação, útil para reencontrar o contrato quando o vínculo local se perdeu. Consulte só o que está pendente há tempo suficiente, com espaçamento, para não gastar cota de requisições com polling agressivo: os limites e os cabeçalhos RateLimit-* que aparecem em respostas 429 estão em rate limits, paginação e idempotência.
Prazos importam aqui. Uma sessão de assinatura vale por padrão 72 horas e um envelope aceita expiresInMinutes entre 5 minutos e 7 dias; passado o prazo, a transação vai para EXPIRED e você recebe SIGNING_SESSION.EXPIRED ou ENVELOPE.EXPIRED. A reconciliação que encontra um EXPIRED deve encerrar o caso localmente e, se fizer sentido no seu fluxo, criar uma nova sessão. Quando a transação tem um prazo de submissão configurado, uma checagem diária da plataforma emite TRANSACTION.DEADLINE_APPROACHING com menos de 48 horas restantes; trate esse evento como um aviso oportuno, não como base da sua lógica de expiração.
Observabilidade: os quatro números que importam
- Latência de resposta do endpoint (p99). Se passar de 2 segundos, alguma coisa entrou no caminho da requisição que deveria estar no worker.
- Idade do evento mais antigo não processado (
now - min(received_at) where processed_at is null). É o seu atraso real. Alarme em 5 minutos. - Eventos aplicados pela reconciliação, não pelo webhook. Zero é o normal. Qualquer valor persistente indica entregas FAILED e merece investigação do seu lado (firewall, certificado TLS, deploy).
- Falhas de HMAC. Uma ou outra é ruído da internet; uma sequência é segredo rotacionado sem atualizar o ambiente, ou alguém testando a sua URL.
Para o primeiro item, lembre-se de que a verificação de assinatura inclui a tolerância de tempo de 5 minutos no X-SignDocs-Timestamp: um servidor com relógio defasado rejeita eventos legítimos e aparece nas métricas como "falha de HMAC". NTP resolve.
Checklist de produção
- Endpoint verifica HMAC sobre o corpo bruto e responde 200 em milissegundos.
- Evento gravado em tabela com
event_id UNIQUEantes de qualquer processamento. - Fila com deduplicação pelo
iddo evento e dead-letter para o que falhar repetidamente. - Worker idempotente, com máquina de estados que só avança.
- Download do documento e das evidências em fila separada, com retries próprios.
- Rotina de reprocessamento para
processed_at IS NULL. - Rotina de reconciliação com
GET /v1/transactions/{id}para pendências antigas. - Evento de teste (
POST /v1/webhooks/{id}/test) no CI e alarmes nos quatro números acima.
Tudo isso pode ser validado no sandbox de homologação, gratuito e sem cartão, onde a biometria é simulada e as entidades expiram em 7 dias. Em produção, o acesso à API é por plano sob medida com o time comercial.
Perguntas Frequentes
Quantas vezes a SignDocs tenta entregar um webhook?
Cada entrega é um POST com timeout de 15 segundos. Erros de rede e respostas 5xx são retentados, até 3 tentativas no total, com espera curta e crescente entre elas. Depois disso a entrega é registrada como FAILED e não é repetida indefinidamente. Respostas 4xx não são retentadas: são tratadas como recusa do seu endpoint. Por isso o consumidor precisa responder rápido, guardar o evento antes de processar e manter uma rotina de reconciliação com GET /v1/transactions/{id} para o que eventualmente não chegar.
Por que devo responder 200 antes de processar o evento?
Porque o processamento (gravar no banco, baixar o PDF, chamar o ERP) tem latência que você não controla, e a janela de entrega é de 15 segundos por tentativa. Se o seu endpoint faz tudo dentro da requisição, um banco lento ou um ERP fora do ar transforma o evento em FAILED. O desenho seguro é: verificar o HMAC sobre o corpo bruto, gravar o evento em uma tabela com event_id único, publicar em uma fila e responder 200 em milissegundos. Um worker consome a fila depois, com retries próprios.
Como evito processar o mesmo evento duas vezes?
Deduplique pelo campo id do evento em duas camadas. A primeira é a fila: SQS FIFO com MessageDeduplicationId, BullMQ com jobId igual ao id, Sidekiq com chave de unicidade. A segunda, definitiva, é uma tabela webhook_events com índice único em event_id e uma coluna processed_at: o worker verifica processed_at dentro da mesma transação de banco em que aplica o evento, e ações não idempotentes (como enviar um e-mail) só acontecem quando ele ainda está nulo.
Os eventos chegam em ordem?
Não há garantia de ordem entre eventos da mesma transação: um STEP.COMPLETED pode chegar depois do SIGNING_SESSION.COMPLETED. Modele o estado do seu contrato como uma máquina que só avança (pendente, em andamento, assinado, encerrado) e trate um evento de estado menor chegando depois de um maior como no-op. Isso torna o worker indiferente à ordem e também ao reprocessamento, já que reaplicar um evento antigo nunca regride o estado.
O que faço quando um evento nunca chegou?
Reconcilie com a fonte da verdade. Uma rotina periódica seleciona os contratos pendentes há mais de alguns minutos e consulta GET /v1/transactions/{id}, aplicando o status devolvido (ACTIVE, COMPLETED, CANCELLED, EXPIRED, FAILED) pela mesma máquina de estados do worker. A resposta traz também o metadata que você enviou na criação, útil para reencontrar o registro local. Consulte só pendências antigas e com espaçamento, para não consumir a cota de requisições com polling agressivo.
Como testo o caminho de reprocessamento antes de precisar dele?
Use POST /v1/webhooks/{webhookId}/test: a plataforma envia um evento de teste real, assinado com o segredo daquele webhook, para a URL registrada, exercitando HMAC, fila, worker e tabela de ponta a ponta. Coloque essa chamada no CI e, uma vez, derrube o worker de propósito para ver a rotina de processed_at IS NULL reenfileirar o evento. Tudo isso funciona no sandbox de homologação, gratuito e sem cartão, com entidades que expiram em 7 dias.
Valide o consumidor de webhooks no sandbox
Credenciais de homologação gratuitas, sem cartão, e o evento de teste POST /v1/webhooks/{id}/test para exercitar HMAC, fila, worker e reconciliação antes de produção. Em produção, plano sob medida com o time comercial.
Criar credenciais de homologação Fale com o time comercial