Webhooks
Um webhook é uma URL da sua aplicação que o Signer chama quando algo acontece com um documento da sua
organização. Por exemplo, o Signer envia um POST para essa URL quando um participante assina um documento
ou quando o fluxo termina.
Use webhooks para acompanhar documentos. Sem webhooks, sua aplicação precisa consultar
GET /api/documents/{id} repetidas vezes até o estado mudar. Essa técnica se chama polling (consulta
periódica). Com webhooks, o Signer avisa a sua aplicação logo depois do evento.
Como funciona
O diagrama mostra o caminho de um evento, desde a ação do participante até a sua aplicação:
Participante Signer Sua aplicação
| | |
|-- assina -------->| |
| |-- POST https://sua-url ---------->| 1. Verifica o cabeçalho de autenticação
| | { "type": ..., "data": ... } |
| |<-- 200 OK ------------------------| 2. Responde na hora
| | | 3. Processa o evento em segundo plano
| | |
Se a sua aplicação não responder 2xx, o Signer tenta enviar o evento de novo mais tarde. A seção
Entrega, retentativas e ordem explica as regras de envio.
Para integrar webhooks, faça estes passos:
- Cadastre a URL do webhook na sua organização.
- Escolha como o Signer autentica a chamada.
- Crie o endpoint que recebe os eventos.
- Use a página Referência dos eventos para ler os dados de cada tipo de evento.
Cadastrar um webhook
- No menu lateral, clique em Organizações e selecione a sua organização.
- Na seção Integração, ative Habilitar webhooks.
- No campo URL, informe a URL que vai receber os eventos. Use HTTPS.
- Em Tipo de autenticação, escolha como o Signer autentica a chamada. Veja Autenticar a chamada.
- Se você escolheu um tipo diferente de Sem autenticação, informe o segredo em Token de Autenticação.
- Clique em Salvar.

- Uma organização pode ter vários webhooks. Todos os webhooks recebem todos os eventos.
- A configuração
MaxWebhooksPerOrganizationda instância define o limite de webhooks por organização. O padrão é 5. Se a organização já está no limite, o Signer não aceita um novo webhook. - Organizações pessoais não têm webhooks.
Autenticar a chamada
O Signer não assina o corpo da requisição. Para mostrar que a chamada vem do Signer, ele envia um cabeçalho com o token que você cadastrou. O Tipo de autenticação define qual cabeçalho o Signer envia:
| Tipo de autenticação | Cabeçalho enviado |
|---|---|
| Sem autenticação | Nenhum |
| Basic | Authorization: Basic <token> |
| Bearer | Authorization: Bearer <token> |
| X-Api-Key | X-Api-Key: <token> |
Use HTTPS e um tipo de autenticação diferente de Sem autenticação. Verifique o cabeçalho em toda requisição antes de processar o evento. Sem essa verificação, qualquer pessoa que conheça a sua URL pode enviar eventos falsos.
O Signer envia o token exatamente como você cadastrou e não codifica nada. No modo Basic, cadastre o
valor já codificado em Base64, no formato usuario:senha.
Receber webhooks no código
Os exemplos abaixo mostram um endpoint mínimo em cada linguagem. Cada endpoint faz quatro coisas, na mesma ordem dos números nos comentários do código:
- Verifica o cabeçalho de autenticação.
- Responde
200imediatamente. - Ignora eventos repetidos.
- Consulta o documento antes de agir.
Os exemplos são versões reduzidas do WebhookEndpointScenario do repositório SignerSamples. Eles não usam
framework web. O C# usa HttpListener, o Java usa o HttpServer do JDK, o Node.js usa o módulo http e o
PHP usa o servidor embutido. Os cenários completos também mostram como testar o endpoint com curl.
- C#
- Java
- Node.js
- PHP
using Lacuna.Signer.Api;
using Newtonsoft.Json.Linq;
// "Bearer <token>": o token cadastrado no webhook, com authType Bearer
private static readonly string ExpectedAuthorization = "Bearer " + Environment.GetEnvironmentVariable("SIGNER_WEBHOOK_TOKEN");
// Chaves dos eventos já recebidos; em produção, use armazenamento durável, como uma tabela com chave única
private readonly ConcurrentDictionary<string, bool> receivedEvents = new ConcurrentDictionary<string, bool>();
// Chamado para cada requisição de um HttpListener iniciado em http://localhost:8080/
private async Task HandleRequestAsync(HttpListenerContext context)
{
var request = context.Request;
var response = context.Response;
// 1. Verifique o cabeçalho de autenticação: o Signer não assina o corpo
if (!IsAuthorized(request.Headers["Authorization"]))
{
response.StatusCode = 401;
response.Close();
return;
}
// O corpo é { "type": "...", "data": { ... } }. Leia-o como JSON, e não como WebhookModel,
// que rejeita os tipos de evento que o seu enum WebhookTypes não lista.
string body;
using (var reader = new StreamReader(request.InputStream, Encoding.UTF8))
{
body = await reader.ReadToEndAsync();
}
var webhook = JObject.Parse(body);
// 3. Seja idempotente. O GetEventKey (do exemplo) junta o type, o id do documento e o flowActionId.
var isNew = receivedEvents.TryAdd(GetEventKey(webhook), true);
// 2. Responda 200 imediatamente (também a um evento repetido, para o Signer parar de enviá-lo) e só então processe
response.StatusCode = 200;
response.Close();
if (!isNew)
{
return;
}
// 4. Não confie na ordem dos eventos: consulte o documento antes de agir
if ((string)webhook["type"] == nameof(WebhookTypes.DocumentConcluded))
{
var document = await signerClient.GetDocumentDetailsAsync((Guid)webhook["data"]["id"]);
if (document.Status == DocumentStatus.Concluded)
{
// Baixe o arquivo assinado, atualize os seus registros...
}
}
}
private static bool IsAuthorized(string authorization)
{
// Compare hashes SHA-256, para que o tempo da comparação não revele quanto do token coincidiu
using (var sha256 = SHA256.Create())
{
var expected = sha256.ComputeHash(Encoding.UTF8.GetBytes(ExpectedAuthorization));
var actual = sha256.ComputeHash(Encoding.UTF8.GetBytes(authorization ?? ""));
return actual.SequenceEqual(expected);
}
}
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.lacunasoftware.signer.DocumentStatus;
import com.lacunasoftware.signer.WebhookTypes;
import com.lacunasoftware.signer.documents.DocumentModel;
import com.lacunasoftware.signer.webhooks.WebhookModel;
import com.sun.net.httpserver.HttpExchange;
// "Bearer <token>": o token cadastrado no webhook, com authType Bearer
private static final String EXPECTED_AUTHORIZATION = "Bearer " + System.getenv("SIGNER_WEBHOOK_TOKEN");
// Chaves dos eventos já recebidos; em produção, use armazenamento durável, como uma tabela com chave única
private final Set<String> receivedEvents = ConcurrentHashMap.newKeySet();
// Processa os eventos depois de respondidos, fora da thread do servidor
private final ExecutorService processor = Executors.newSingleThreadExecutor();
// O handler de HttpServer.create(new InetSocketAddress("localhost", 8080), 0).createContext("/webhook", ...)
private void handleRequest(HttpExchange exchange) throws IOException {
// 1. Verifique o cabeçalho de autenticação: o Signer não assina o corpo. MessageDigest.isEqual compara em tempo constante.
String authorization = exchange.getRequestHeaders().getFirst("Authorization");
if (authorization == null || !MessageDigest.isEqual(
authorization.getBytes(StandardCharsets.UTF_8),
EXPECTED_AUTHORIZATION.getBytes(StandardCharsets.UTF_8))) {
exchange.sendResponseHeaders(401, -1);
exchange.close();
return;
}
// O corpo é { "type": "...", "data": { ... } }. O signerClient.getGson() lê os enums e as datas da API.
Gson gson = signerClient.getGson();
JsonElement body;
try (InputStreamReader reader = new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8)) {
body = gson.fromJson(reader, JsonElement.class);
}
// 3. Seja idempotente. O getEventKey (do exemplo) junta o type, o id do documento e o flowActionId.
boolean isNew = receivedEvents.add(getEventKey(gson.fromJson(body, Event.class)));
// 2. Responda 200 imediatamente (também a um evento repetido, para o Signer parar de enviá-lo) e só então processe
exchange.sendResponseHeaders(200, -1);
exchange.close();
if (isNew) {
processor.execute(() -> processEvent(gson, body));
}
}
private void processEvent(Gson gson, JsonElement body) {
try {
WebhookModel webhook = gson.fromJson(body, WebhookModel.class);
if (webhook.getType() == WebhookTypes.DOCUMENTCONCLUDED) {
// 4. Não confie na ordem dos eventos: consulte o documento antes de agir
String documentId = body.getAsJsonObject().getAsJsonObject("data").get("id").getAsString();
DocumentModel document = signerClient.getDocumentDetails(UUID.fromString(documentId));
if (document.getStatus() == DocumentStatus.CONCLUDED) {
// Baixe o arquivo assinado, atualize os seus registros...
}
}
} catch (Exception e) {
// Já respondido, o Signer não o enviará de novo: guarde o evento e tente de novo a partir do seu armazenamento
}
}
import * as crypto from "crypto";
import * as http from "http";
import { DocumentStatus, WebhooksWebhookModel, WebhookTypes } from "signer-node-client";
// "Bearer <token>": o token cadastrado no webhook, com authType Bearer
const expectedAuthorization = "Bearer " + process.env.SIGNER_WEBHOOK_TOKEN;
// Chaves dos eventos já recebidos; em produção, use armazenamento durável, como uma tabela com chave única
const receivedEvents = new Set<string>();
http.createServer((req, res) => {
// 1. Verifique o cabeçalho de autenticação: o Signer não assina o corpo
if (!isAuthorized(req.headers.authorization)) {
res.writeHead(401).end();
return;
}
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
// O corpo é { "type": "...", "data": { ... } }
let webhook: WebhooksWebhookModel;
let key: string;
try {
webhook = JSON.parse(Buffer.concat(chunks).toString("utf8"));
// 3. Seja idempotente. O getEventKey (do exemplo) junta o type, o id do documento e o flowActionId.
key = getEventKey(webhook);
} catch {
res.writeHead(400).end();
return;
}
const isNew = !receivedEvents.has(key);
receivedEvents.add(key);
// 2. Responda 200 imediatamente (também a um evento repetido, para o Signer parar de enviá-lo) e só então processe
res.writeHead(200).end();
if (isNew) {
processEvent(webhook).catch((err) => console.error(`Could not process ${key}: ${err.message}`));
}
});
}).listen(8080, "localhost");
function isAuthorized(authorization: string | undefined): boolean {
// timingSafeEqual exige entradas do mesmo tamanho, então compare hashes SHA-256
const hash = (value: string) => crypto.createHash("sha256").update(value).digest();
return crypto.timingSafeEqual(hash(authorization ?? ""), hash(expectedAuthorization));
}
async function processEvent(webhook: WebhooksWebhookModel) {
if (webhook.type === WebhookTypes.DocumentConcluded) {
// 4. Não confie na ordem dos eventos: consulte o documento antes de agir
const document = (await documentsApi.apiDocumentsIdGet(webhook.data.id)).data;
if (document.status === DocumentStatus.Concluded) {
// Baixe o arquivo assinado, atualize os seus registros...
}
}
}
// webhook.php, servido com: php -S localhost:8080 webhook.php
use Lacuna\Signer\Model\DocumentsDocumentModel;
use Lacuna\Signer\Model\DocumentStatus;
use Lacuna\Signer\Model\WebhookTypes;
// 1. Verifique o cabeçalho de autenticação: o Signer não assina o corpo. hash_equals compara em tempo constante.
// "Bearer <token>": o token cadastrado no webhook, com authType Bearer
if (!hash_equals('Bearer ' . getenv('SIGNER_WEBHOOK_TOKEN'), $_SERVER['HTTP_AUTHORIZATION'] ?? '')) {
http_response_code(401);
return;
}
// O corpo é { "type": "...", "data": { ... } }
$webhook = json_decode(file_get_contents('php://input'), true);
if (!is_array($webhook) || !is_string($webhook['type'] ?? null) || $webhook['type'] === '') {
http_response_code(400);
return;
}
// 3. Seja idempotente. O getEventKey (do exemplo) junta o type, o id do documento e o flowActionId;
// o recordEvent devolve false para uma chave já registrada (o exemplo cria um arquivo por chave;
// em produção, use armazenamento durável, como uma tabela com chave única).
if (!recordEvent(getEventKey($webhook))) {
http_response_code(200);
return;
}
// 2. Responda 200 imediatamente e só então processe o evento
ignore_user_abort(true);
http_response_code(200);
header('Content-Length: 0');
header('Connection: close');
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); // PHP-FPM
} else {
// Outros servidores, como o embutido do PHP: envie os cabeçalhos agora
while (ob_get_level() > 0) {
ob_end_flush();
}
flush();
}
// 4. Não confie na ordem dos eventos: consulte o documento antes de agir
if ($webhook['type'] === WebhookTypes::DOCUMENT_CONCLUDED) {
$document = new DocumentsDocumentModel($signerClient->getDocumentDetails($webhook['data']['id']));
if ($document->getStatus() === DocumentStatus::CONCLUDED) {
// Baixe o arquivo assinado, atualize os seus registros...
}
}
Veja os endpoints completos: C# · Java · PHP · Node.js. Para criar o cliente usado nos exemplos, veja Criar documentos.
O enum WebhookTypes do cliente .NET e do cliente PHP não tem DocumentDeleted. Isso vale para o
Lacuna.Signer.Api 1.67.0, que o Lacuna.Signer.Client 1.24.0 instala, e para o cliente PHP 1.4.10. Os
enums dos clientes Java e Node.js têm DocumentDeleted.
Se o enum da sua biblioteca não lista um tipo de evento, compare o type do corpo como texto. Os exemplos
C# e PHP acima fazem essa comparação.
Eventos
O corpo de toda requisição tem o formato { "type": ..., "data": ... }. O campo type diz o tipo do
evento, como DocumentSigned ou DocumentConcluded. O conteúdo de data muda conforme o tipo.
Para ver a lista dos tipos, os campos de cada evento e exemplos de corpo, veja a página Referência dos eventos.
Entrega, retentativas e ordem
Esta seção explica como o Signer envia os eventos. A maior parte dos problemas de integração vem dessas regras.
Entrega assíncrona
O Signer não envia o evento no mesmo instante da ação do usuário. Um processo em segundo plano envia cada evento, em uma fila de baixa prioridade. Por isso, um pequeno atraso é normal.
Se a organização tem mais de um webhook, o Signer envia o evento para cada webhook separadamente. Uma falha em um webhook não afeta os outros.
Retentativas
Sua aplicação deve responder com um código 2xx. Se ela responder com outro código, ou se a conexão falhar,
o Signer marca a entrega como falha. Depois, o Signer tenta enviar o evento de novo, até 10 vezes. O tempo
de espera aumenta a cada tentativa:
| Nova tentativa | Espera depois da falha anterior |
|---|---|
| 1ª | 15 a 45 segundos |
| 2ª | 15 segundos a 1 minuto e 15 segundos |
| 3ª | 30 segundos a 2 minutos |
| 4ª | 1 minuto e 35 segundos a 3 minutos e 30 segundos |
| 5ª | 4 minutos e 30 segundos a 7 minutos |
| 6ª | 10 minutos e 40 segundos a 13 minutos e 30 segundos |
| 7ª | 22 a 25 minutos |
| 8ª | 40 a 44 minutos |
| 9ª | 1 hora e 8 minutos a 1 hora e 13 minutos |
| 10ª | 1 hora e 50 minutos a 1 hora e 55 minutos |
As 10 novas tentativas levam, no total, de 4 horas e 20 minutos a 4 horas e 45 minutos depois da primeira falha. O tempo exato de cada espera tem uma parte aleatória. Por isso, a tabela mostra faixas. Depois da 10ª falha, o Signer não envia mais aquele evento.
Em cada nova tentativa, o Signer monta o evento de novo, com os dados atuais do documento. A verificação de estado descrita em Ordem dos eventos também vale para cada nova tentativa.
O Signer registra no log o status e o corpo da resposta de cada falha. Se você investigar um problema de entrega, peça esses registros ao administrador da instância.
Entregas repetidas
Por causa das retentativas, o mesmo evento pode chegar mais de uma vez. Torne o processamento
idempotente, isto é, seguro para executar mais de uma vez com o mesmo evento. Para reconhecer um evento
já processado, use o type, o id do documento e, se existir, o flowActionId.
Ordem dos eventos
O Signer não garante a ordem dos eventos, porque processos independentes fazem as entregas. Por exemplo, o
DocumentConcluded de um documento pode chegar antes do DocumentSigned do mesmo documento.
Antes de enviar alguns eventos, o Signer verifica se o documento ainda está no estado do evento. Se o
estado mudou, o Signer descarta o evento. Por exemplo, se alguém pausar e retomar um documento logo em
seguida, sua aplicação pode receber só o DocumentResumed, sem o DocumentPaused. Essa verificação vale
para DocumentConcluded, DocumentCanceled, DocumentPaused, DocumentResumed e DocumentExpired.
Boas práticas
- Responda
200imediatamente e processe o evento depois, em segundo plano. Se o endpoint demorar, a chamada passa do tempo limite (timeout) e o Signer tenta de novo sem necessidade. - Verifique o cabeçalho de autenticação em toda requisição.
- Mantenha a URL estável e acessível. O Signer precisa chegar à URL pela internet. Em instalações on-premises, o Signer precisa chegar à URL pela rede onde o servidor dele está.
- Registre os eventos recebidos. Esse registro ajuda a encontrar diferenças entre o seu sistema e o Signer.
Solução de problemas
Se você precisar de mais detalhes sobre uma falha de entrega, peça os logs do Signer ao administrador da instância. Para cada falha, o log mostra o código de status, o corpo da resposta e a URL chamada.
Se você corrigir a URL ou o token de um webhook, as próximas tentativas de um evento que falhou já usam o valor novo. O Signer lê a configuração do webhook a cada tentativa.
Sua aplicação não recebe nenhum evento
| Causa possível | Como resolver |
|---|---|
| A opção Habilitar webhooks está desativada. | Em Organizações, seção Integração, ative Habilitar webhooks e clique em Salvar. |
| O webhook foi cadastrado depois da ação no documento. | O Signer envia o evento só para os webhooks que existem no momento da ação. Ele não envia eventos de ações anteriores ao cadastro. |
| O documento está em uma organização pessoal. | Organizações pessoais não têm webhooks. Crie o documento em uma organização que tenha o webhook cadastrado. |
| O Signer não consegue se conectar à URL. Por exemplo, o nome do servidor não existe, um firewall bloqueia a porta ou a URL só funciona na sua rede interna. | Verifique se a URL responde a partir de uma máquina fora da sua rede, por exemplo com curl -X POST <url>. Em instalações on-premises, faça o teste a partir do servidor do Signer. |
| O certificado HTTPS da URL não é válido. Por exemplo, ele é autoassinado, expirou ou não traz a cadeia completa. | O Signer recusa a conexão quando o certificado não é válido. Use um certificado emitido por uma autoridade certificadora confiável e configure a cadeia completa no servidor. |
Sua aplicação responde 401 ou 403
| Causa possível | Como resolver |
|---|---|
O Tipo de autenticação do webhook não é o que o seu endpoint espera. Por exemplo, o webhook usa X-Api-Key e o endpoint lê o cabeçalho Authorization. | Escolha no webhook o tipo que o endpoint lê. Veja os cabeçalhos de cada tipo em Autenticar a chamada. |
| O token cadastrado no webhook é diferente do token que o endpoint compara. | Cadastre de novo o token no webhook e no endpoint. Verifique se não há espaço ou quebra de linha no começo ou no fim do token. |
| No modo Basic, o token não está codificado em Base64. | O Signer envia o token sem codificar. Cadastre o valor de usuario:senha já codificado em Base64. |
| Um proxy ou gateway entre o Signer e a sua aplicação remove ou troca o cabeçalho de autenticação. | Verifique a configuração do proxy ou do gateway. Ele deve passar o cabeçalho Authorization ou X-Api-Key para a sua aplicação. |
O mesmo evento chega várias vezes
| Causa possível | Como resolver |
|---|---|
Sua aplicação respondeu com um código diferente de 2xx. | Responda 200 também para um evento que você já processou. Se você responder com erro, o Signer tenta de novo. |
| Sua aplicação demorou mais de 100 segundos para responder. | O Signer cancela a chamada depois de 100 segundos e tenta de novo. Responda 200 logo que receber o evento e processe o evento depois, em segundo plano. |
| As retentativas são normais. | Torne o processamento idempotente. Veja Entregas repetidas. |
Sua aplicação recebe um GET ou um corpo vazio
| Causa possível | Como resolver |
|---|---|
A URL cadastrada responde com um redirecionamento (301 ou 302). Ao seguir esse redirecionamento, o Signer troca o POST por um GET, sem corpo. | Cadastre a URL final, que responde sem redirecionar. Confira o protocolo (https), o domínio (com ou sem www) e a barra no fim do caminho. |
A URL redireciona de https para http. | O Signer não segue esse redirecionamento, e a entrega falha. Cadastre a URL https final. |
Um evento esperado não chegou
| Causa possível | Como resolver |
|---|---|
| O estado do documento mudou antes do envio. Por exemplo, alguém pausou e retomou o documento logo em seguida. | O Signer descarta o evento quando o documento já não está no estado do evento. Veja Ordem dos eventos. |
| Alguém removeu o webhook antes do envio. | O Signer não envia o evento para um webhook removido. |
| As 10 novas tentativas falharam. | O Signer não envia mais esse evento. Corrija a causa da falha para que os próximos eventos cheguem. Veja Retentativas. |
Outros problemas
| Problema | Causa e solução |
|---|---|
| O Signer não aceita um novo webhook. | A organização chegou ao limite de webhooks, definido pela configuração MaxWebhooksPerOrganization (5 por padrão). Remova um webhook ou peça ao administrador da instância para mudar o limite. |
A lista documents de um DocumentDeleted não traz todos os documentos excluídos. | Em exclusões da organização ou de uma pasta, a lista traz no máximo 100 documentos. Veja Referência dos eventos. |