Webhooks
Webhooks são a forma do seu sistema saber sozinho que algo aconteceu com
um documento — sem precisar ficar chamando GET /v1/documents/{id} de
tempos em tempos. O Firmou avisa o seu endereço a cada evento relevante da
cerimônia.
Não existe endpoint na API para cadastrar um webhook — cadastro é ato de gente autenticada por sessão, dentro do Firmou, pela mesma razão de tokens não se emitirem sozinhos: veja Autenticação.
Cadastrando um destino
- Entre no Firmou e vá em Configurações › Webhooks.
- Clique em Novo destino e informe a URL do seu endereço. Precisa ser
pública — endereços internos (
localhost, IP privado, nomes sem domínio) são recusados, e em produção a URL precisa serhttps://. - Marque os eventos que interessam. Não existe "quero tudo": um endpoint que recebe qualquer evento fica refém de todo evento novo que o Firmou vier a criar.
- Escolha o workspace, se quiser limitar o alcance. Deixando em "Todos os workspaces", o destino recebe aviso de qualquer documento do cliente; escolhendo um, recebe só dos documentos criados nele. É o que separa o ERP que cuida de contrato do sistema de pessoal que cuida de admissão — sem isso, cada um receberia o documento do outro, com nome, e-mail e telefone de quem assinou dentro. Dá para mudar depois, a qualquer momento.
- Copie o segredo mostrado na hora. Ele não aparece de novo — é o que assina cada aviso que chega, e é assim que o seu sistema confirma que o POST veio do Firmou e não de qualquer um que descobriu a URL.
O aviso
Um POST com JSON, para a URL cadastrada:
{
"event": "signed",
"occurred_at": "2026-08-20T14:31:12-03:00",
"document": {
"id": "019fd38e-d163-7347-a030-b34b261663dc",
"title": "Contrato de prestação de serviços",
"status": "pending_signatures",
"validation_url": "https://firmou.app/validar/019fd38e-d163-7347-a030-b34b261663dc"
},
"recipient": {
"id": "0e2f9a1c-...",
"name": "Fulano de Tal",
"email": "fulano@example.com",
"phone": null,
"status": "signed"
}
}
document.id é o uuid_public — o mesmo código impresso no documento e
usado em GET /v1/documents/{id}, não o id
interno. recipient vem null em eventos que são do documento como um
todo (document_completed, document_sent, document_canceled,
seal_failed) — os demais são sobre uma pessoa específica.
Não vai IP nem navegador de ninguém, nem o conteúdo do documento — o aviso sai para um endereço que o Firmou não controla, e vazar isso ali seria um vazamento com o nome do Firmou junto.
Eventos
| Código | O que significa |
|---|---|
document_completed | Todos assinaram e o arquivo final está pronto |
document_sent | Os convites saíram para os signatários |
document_canceled | O emissor encerrou o documento antes do fim |
seal_failed | Todos assinaram, mas o selo não foi aplicado — o documento está parado |
opened | O signatário abriu o link pela primeira vez |
consent_given | O signatário aceitou o meio de assinatura e pode assinar |
signed | Um signatário assinou. Chega uma vez por pessoa |
declined | Um signatário recusou — o documento não se conclui |
identity_verified | O signatário conferiu o código de verificação |
identity_verification_failed | Código errado — útil para detectar tentativa de acesso indevido |
link_expired | O prazo para assinar terminou sem a assinatura |
signing_means_revoked | O link daquele signatário foi invalidado por comprometimento |
certificate_validation_failed | O certificado ICP-Brasil apresentado não passou nas conferências |
document_completed não existe na trilha interna do documento — para quem
integra, "o documento ficou pronto" é um fato só, ainda que por dentro ele
possa vir de mais de um caminho.
Verificando a assinatura
Três cabeçalhos chegam em toda entrega:
| Cabeçalho | Conteúdo |
|---|---|
X-Firmou-Event | O mesmo valor de event no corpo |
X-Firmou-Timestamp | Quando o aviso foi montado (epoch, segundos) |
X-Firmou-Signature | HMAC-SHA256 de {timestamp}.{corpo}, com o segredo do destino |
O timestamp entra no cálculo — e não só ao lado dele — porque sem isso um aviso capturado hoje continuaria válido para sempre: reenviado amanhã, a assinatura ainda bateria sobre um fato antigo.
$esperado = hash_hmac(
'sha256',
$_SERVER['HTTP_X_FIRMOU_TIMESTAMP'].'.'.file_get_contents('php://input'),
$segredoDoWebhook,
);
if (! hash_equals($esperado, $_SERVER['HTTP_X_FIRMOU_SIGNATURE'])) {
http_response_code(401);
exit;
}
Use hash_equals() (ou equivalente na sua linguagem) — uma comparação
=== normal vaza timing e é o tipo de detalhe que vira falha de segurança
séria bem depois de todo mundo ter esquecido dele.
Recuse também o aviso cujo X-Firmou-Timestamp tenha mais de cinco
minutos. A assinatura sozinha não distingue o aviso que acabou de sair do
aviso legítimo que alguém guardou e reenviou depois; a janela curta é o que
fecha essa porta.
Sem essa conferência o seu endpoint aceita um POST de qualquer um que
descubra a URL — e o que ele diz costuma disparar pagamento ou liberação de
acesso do seu lado.
Reentrega e desligamento automático
Uma entrega que falha (erro de rede, timeout, qualquer status HTTP fora de 2xx) é tentada de novo, até 6 vezes, com espera crescente: 10s, 1min, 5min, 30min, 2h. Cobre o reinício rápido, a manutenção de alguns minutos e a madrugada de quem só olha o alerta de manhã.
Depois de 20 falhas seguidas, o destino é desligado sozinho — um endpoint morto não pode consumir fila para sempre. A tela de Webhooks mostra o motivo do desligamento; religar zera a contagem de falhas.
Próximo passo
Com o destino cadastrado, veja a Referência da API
para saber o que cada evento tem a ver com o que você acabou de enviar em
POST /v1/documents.