Webhooks permitem que sua aplicação receba notificações em tempo real sobre eventos que ocorrem na plataforma SignDocs Brasil. Em vez de realizar polling periódico na API, você registra uma URL HTTPS e o SignDocs Brasil envia requisições HTTP POST automaticamente sempre que um evento relevante acontece.
Casos de uso típicos:
Cada entrega de webhook inclui uma assinatura HMAC-SHA256 que permite ao receptor verificar a autenticidade e a integridade do payload.
🧭 Pela API ou pelo painel? Você pode registrar webhooks com as chamadas desta seção ou pela aba Webhooks do API Dashboard. O painel acrescenta o histórico de entregas com latência e um botão Reenviar por entrega; a API acrescenta o disparo de payload de teste (
POST /v1/webhooks/{webhookId}/test, §8), que o painel não expõe.
Para começar a receber notificações, registre uma URL de webhook com os tipos de evento desejados.
POST /v1/webhooks
Authorization: Bearer {access_token}
Content-Type: application/json
{
"url": "https://sua-aplicacao.com.br/webhooks/signdocs",
"events": [
"TRANSACTION.COMPLETED",
"TRANSACTION.FAILED",
"QUOTA.WARNING"
]
}
| Campo | Regra |
|---|---|
url |
Obrigatório. Deve ser uma URL válida com protocolo HTTPS. |
events |
Obrigatório. Array não vazio contendo tipos de evento válidos. |
{
"webhookId": "wh_01HX9Z3KQWERTY1234567890",
"url": "https://sua-aplicacao.com.br/webhooks/signdocs",
"secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2",
"events": [
"TRANSACTION.COMPLETED",
"TRANSACTION.FAILED",
"QUOTA.WARNING"
],
"status": "ACTIVE",
"createdAt": "2025-01-15T10:00:00.000Z"
}
IMPORTANTE: O campo
secreté retornado apenas uma vez, no momento da criação. Armazene-o de forma segura (por exemplo, em um cofre de segredos ou variável de ambiente protegida). Ele será necessário para verificar a assinatura HMAC de cada entrega. Não é possível recuperá-lo posteriormente — caso seja perdido, será necessário excluir o webhook e criar um novo.
O token de acesso deve conter o escopo webhooks:write.
O contrato declara 20 tipos de evento, e todos têm um produtor no código. Se você se inscrever em um deles e a entrega nunca chegar, é defeito nosso — não é uma expectativa a calibrar.
Nem sempre foi assim. Até setembro de 2026 seis eventos estavam no enum sem nada que os
emitisse. Três viraram código (TRANSACTION.FAILED, QUOTA.WARNING,
API.DEPRECATION_NOTICE) e três foram retirados: STEP.STARTED,
STEP.COMPLETED e STEP.FAILED. Etapa é unidade interna do motor de
política — em 500 sessões/mês os três produziriam várias entregas por sessão para dizer o que
SIGNING_SESSION.COMPLETED e TRANSACTION.FALLBACK já dizem uma vez.
Se o seu código os declarava, remova-os do array events; a inscrição passa a ser
recusada com 400.
| Tipo | Descrição |
|---|---|
TRANSACTION.CREATED |
Uma nova transação foi criada. |
TRANSACTION.COMPLETED |
Todas as etapas foram concluídas com sucesso e o Evidence Pack foi gerado. |
TRANSACTION.CANCELLED |
A transação foi cancelada (por chamada de API ou ação administrativa). |
TRANSACTION.FAILED |
Falha terminal: todas as etapas passaram e a finalização ainda assim não produziu documento assinado. Ver a nota abaixo. |
TRANSACTION.EXPIRED |
A transação expirou sem que todas as etapas fossem concluídas dentro do prazo. |
TRANSACTION.FALLBACK |
A transação caiu para a política alternativa após falha na primária. |
TRANSACTION.DEADLINE_APPROACHING |
(NT65) Faltam ≤2 dias úteis para o prazo de submissão ao INSS. |
Sobre TRANSACTION.FAILED: ele existe só no fluxo hospedado.
Uma etapa que falha não é uma transação que falhou — o signatário tenta de novo e a sessão segue
ACTIVE. O evento marca o caso em que não há mais ninguém para tentar: a finalização
quebrou depois de todas as etapas passarem, e o navegador que repetiria a chamada já recebeu uma
sessão morta. Na API direta o erro volta na resposta de
POST /v1/transactions/{id}/finalize e você mesmo repete a chamada, por isso ali não
há evento.
| Tipo | Descrição |
|---|---|
STEP.PURPOSE_DISCLOSURE_SENT |
(NT65) Notificação de divulgação de finalidade enviada ao beneficiário. |
| Tipo | Descrição |
|---|---|
SIGNING_SESSION.CREATED |
Uma nova sessão de assinatura expressa foi criada. |
SIGNING_SESSION.COMPLETED |
A sessão foi concluída — todas as etapas finalizadas e Evidence Pack gerado. |
SIGNING_SESSION.CANCELLED |
A sessão foi cancelada pelo integrador via API. |
SIGNING_SESSION.EXPIRED |
A sessão expirou antes de ser concluída pelo signatário. |
| Tipo | Descrição |
|---|---|
ENVELOPE.CREATED |
Envelope multi-assinante criado. |
ENVELOPE.ALL_SIGNED |
Todas as assinaturas do envelope foram concluídas. |
ENVELOPE.CANCELLED |
Envelope cancelado; as assinaturas pendentes são encerradas. |
ENVELOPE.EXPIRED |
Envelope expirou com uma ou mais assinaturas pendentes. |
| Tipo | Descrição |
|---|---|
ENROLLMENT.EXPIRING |
A foto de referência será destruída em breve — 30 e 7 dias antes. data.daysUntilExpiry diz qual. |
ENROLLMENT.EXPIRED |
A foto de referência foi destruída hoje; o usuário precisa de novo cadastro para assinar. |
| Tipo | Descrição |
|---|---|
QUOTA.WARNING |
O uso mensal de uma dimensão cruzou 80% do limite do plano. Uma vez por dimensão por mês. |
Só mensal. As cotas diárias são trava rígida, não honram overageAllowance e
reiniciam toda noite — avisar sobre elas produziria notificação quase todo dia, que é como um
aviso deixa de ser lido. O evento dispara na requisição que atravessa a marca, não a cada chamada
acima dela.
| Tipo | Descrição |
|---|---|
API.DEPRECATION_NOTICE |
Você chamou um endpoint marcado como deprecated. Uma vez por endpoint por dia. |
Carrega os mesmos dados dos cabeçalhos Deprecation e Sunset
(RFC 8594), que já vinham em toda resposta da rota — só que entregues onde alguém os lê. Cabeçalho
de resposta de chamada servidor-a-servidor não é lido por ninguém; quem precisa agendar a migração
descobria na data do sunset, quando a rota parava de responder.
Ao registrar o webhook, inclua no array events apenas os tipos relevantes para o seu caso de uso. Isso reduz o volume de requisições e facilita o processamento no seu lado.
Todas as entregas de webhook seguem a mesma estrutura JSON de envelope. O campo data varia conforme o tipo de evento.
{
"id": "del_01HX9Z3KQWERTY1234567890",
"eventType": "TRANSACTION.COMPLETED",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"timestamp": "2025-01-15T14:30:00.000Z",
"data": { }
}
| Campo | Tipo | Descrição |
|---|---|---|
id |
string |
Identificador único da entrega (prefixo del_). |
eventType |
string |
Tipo do evento (veja seção 3). |
tenantId |
string |
Identificador do tenant proprietário. |
transactionId |
string |
Identificador da transação associada (ausente em eventos de cota/API). |
timestamp |
string |
Data/hora do evento em UTC, formato ISO 8601. |
data |
object |
Dados específicos do evento (veja exemplos abaixo). |
test |
boolean |
Presente e true apenas em entregas de teste (seção 8). |
Disparado quando todas as etapas de uma transação foram concluídas e o Evidence Pack (.p7m) foi gerado.
{
"id": "del_01HX9Z4MABCDE1234567890",
"eventType": "TRANSACTION.COMPLETED",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"timestamp": "2025-01-15T14:30:00.000Z",
"data": {
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"status": "COMPLETED",
"evidenceId": "ev_01HX9Z4NFGHIJ0987654321",
"signer": {
"name": "Maria Silva",
"userExternalId": "usr_42"
},
"completedAt": "2025-01-15T14:30:00.000Z"
}
}
Disparado quando a finalização quebra depois de todas as etapas terem passado,
no fluxo hospedado. Não é "o signatário errou três vezes" — isso deixa a sessão ACTIVE
e não gera evento. É o caso em que a sessão foi encerrada como FAILED e ninguém vai
repetir a tentativa.
{
"id": "del_01HX9Z5PQRSTU2345678901",
"eventType": "TRANSACTION.FAILED",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"timestamp": "2025-01-15T15:10:00.000Z",
"data": {
"sessionId": "ss_01HX9Z3KQWERTY1234567890",
"status": "FAILED",
"reason": "FINALIZATION_FAILED",
"detail": "Signing certificate unavailable",
"failedAt": "2025-01-15T15:10:00.000Z"
}
}
O documento não foi assinado e a sessão não volta atrás: crie uma nova para o mesmo
signatário. detail é diagnóstico, não texto para o usuário final.
Disparado na requisição que leva o uso mensal de uma dimensão a 80% do limite do plano — uma vez
por dimensão, por mês. Sem transactionId: o evento é sobre o tenant, não sobre uma
transação.
{
"id": "del_01HX9Z6QVWXYZ3456789012",
"eventType": "QUOTA.WARNING",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"timestamp": "2025-01-15T14:25:00.000Z",
"data": {
"dimension": "monthlyBiometric",
"used": 400,
"limit": 500,
"percentUsed": 80,
"threshold": 80,
"period": "PRODUCTION#2025-01",
"resetInSeconds": 1416600
}
}
limit é o limite do plano, sem a tolerância (overageAllowance) — o 429
só chega em plano + tolerância. dimension nomeia o campo do plano, não o contador
interno: monthlyDocuments, monthlyBiometric,
monthlyTransactions, monthlyOtp, monthlyClick,
monthlySigning, monthlySerproIdentity,
monthlyVerification, monthlyTrustSessions.
Disparado na primeira chamada do dia a um endpoint deprecated. Sem transactionId,
pelo mesmo motivo.
{
"id": "del_01HX9Z7RWXYZA4567890123",
"eventType": "API.DEPRECATION_NOTICE",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"timestamp": "2025-01-15T09:02:11.000Z",
"data": {
"endpoint": "POST /v1/exemplo/antigo",
"method": "POST",
"path": "/v1/exemplo/antigo",
"deprecatedAt": "2025-09-01T00:00:00Z",
"sunsetAt": "2026-09-01T00:00:00Z",
"replacement": "POST /v1/exemplo/novo",
"message": "Descrição curta do que mudou."
}
}
sunsetAt é a data em que a rota para de responder. replacement e
message podem estar ausentes quando o registro não os declara.
Cada entrega de webhook inclui dois headers que permitem verificar a autenticidade e a integridade do payload.
| Header | Descrição |
|---|---|
X-SignDocs-Signature |
HMAC-SHA256 do payload, codificado em hexadecimal. |
X-SignDocs-Timestamp |
Timestamp Unix (segundos) utilizado na entrada da assinatura. |
X-SignDocs-Webhook-Id |
Identificador do webhook que originou a entrega. |
Content-Type |
Sempre application/json. |
User-Agent |
SignDocs-Webhook/1.0. |
O SignDocs Brasil constrói a entrada de assinatura concatenando o timestamp e o payload JSON com um ponto como separador:
signing_input = timestamp + "." + payload_json
Em seguida, calcula o HMAC-SHA256 usando o secret fornecido na criação do webhook:
signature = HMAC-SHA256(secret, signing_input) -> hex digest
X-SignDocs-Signature e X-SignDocs-Timestamp da requisição.|agora - timestamp| e rejeitar se a diferença for superior a 300 segundos (5 minutos). Isso previne ataques de replay.timestamp + "." + body_cru e gerar o HMAC-SHA256 com o secret armazenado.const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-signdocs-signature'];
const timestamp = req.headers['x-signdocs-timestamp'];
const body = req.body; // raw body string (não parsed)
// 1. Verificar frescura do timestamp (tolerância de 300 segundos)
const now = Math.floor(Date.now() / 1000);
const ts = parseInt(timestamp, 10);
if (Math.abs(now - ts) > 300) {
throw new Error('Timestamp fora da tolerância. Possível replay attack.');
}
// 2. Recalcular a assinatura
const signingInput = `${timestamp}.${body}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signingInput)
.digest('hex');
// 3. Comparação timing-safe
const sigBuffer = Buffer.from(signature, 'hex');
const expBuffer = Buffer.from(expected, 'hex');
if (sigBuffer.length !== expBuffer.length) {
throw new Error('Assinatura inválida.');
}
if (!crypto.timingSafeEqual(sigBuffer, expBuffer)) {
throw new Error('Assinatura inválida.');
}
return true; // Payload autêntico
}
// Uso com Express (body como raw string):
// app.use('/webhooks/signdocs', express.raw({ type: 'application/json' }));
// app.post('/webhooks/signdocs', (req, res) => {
// try {
// verifyWebhook(req, process.env.SIGNDOCS_WEBHOOK_SECRET);
// const event = JSON.parse(req.body);
// // processar evento...
// res.status(200).send('OK');
// } catch (err) {
// res.status(401).send(err.message);
// }
// });
import hmac
import hashlib
import time
def verify_webhook(headers: dict, body: bytes, secret: str) -> bool:
"""
Verifica a assinatura HMAC-SHA256 de um webhook SignDocs Brasil.
:param headers: Dicionário com os headers da requisição.
:param body: Corpo da requisição como bytes (raw).
:param secret: Secret HMAC obtido na criação do webhook.
:returns: True se a assinatura for válida.
:raises ValueError: Se a verificação falhar.
"""
signature = headers.get("X-SignDocs-Signature", "")
timestamp = headers.get("X-SignDocs-Timestamp", "")
# 1. Verificar frescura do timestamp (tolerância de 300 segundos)
now = int(time.time())
ts = int(timestamp)
if abs(now - ts) > 300:
raise ValueError("Timestamp fora da tolerância. Possível replay attack.")
# 2. Recalcular a assinatura
signing_input = f"{timestamp}.{body.decode('utf-8')}"
expected = hmac.new(
secret.encode("utf-8"),
signing_input.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# 3. Comparação timing-safe
if not hmac.compare_digest(signature, expected):
raise ValueError("Assinatura inválida.")
return True
# Uso com Flask:
# from flask import Flask, request
#
# app = Flask(__name__)
# WEBHOOK_SECRET = os.environ["SIGNDOCS_WEBHOOK_SECRET"]
#
# @app.route("/webhooks/signdocs", methods=["POST"])
# def handle_webhook():
# try:
# verify_webhook(request.headers, request.get_data(), WEBHOOK_SECRET)
# event = request.get_json()
# # processar evento...
# return "OK", 200
# except ValueError as e:
# return str(e), 401
Dica: Certifique-se de utilizar o body cru (raw) da requisição para a verificação, antes de qualquer parsing JSON. Frameworks como Express (Node.js) e Flask (Python) podem alterar a representação do body se você usar middleware de JSON parsing antes da verificação.
Quando o endpoint do webhook retorna um código HTTP fora da faixa 2xx, ou quando a conexão falha, o SignDocs Brasil reenvia a notificação utilizando backoff exponencial.
| Tentativa | Atraso após falha | Tempo acumulado |
|---|---|---|
| 1a | Imediata | 0 |
| 2a | 30 segundos | 30s |
| 3a | 2 minutos | 2min 30s |
| 4a | 10 minutos | 12min 30s |
| 5a | 30 minutos | 42min 30s |
| 6a (max) | 2 horas | 2h 42min 30s |
Devido à política de retry, seu endpoint pode receber o mesmo evento mais de uma vez. Use o campo id do payload como chave de idempotência para evitar processamento duplicado.
Após falhas consecutivas persistentes (todas as tentativas esgotadas repetidamente), o webhook pode ser automaticamente desativado. Verifique o status do webhook periodicamente via GET /v1/webhooks.
Retorna todos os webhooks registrados para o tenant autenticado.
GET /v1/webhooks
Authorization: Bearer {access_token}
{
"webhooks": [
{
"webhookId": "wh_01HX9Z3KQWERTY1234567890",
"url": "https://sua-aplicacao.com.br/webhooks/signdocs",
"events": [
"TRANSACTION.COMPLETED",
"TRANSACTION.FAILED",
"QUOTA.WARNING"
],
"status": "ACTIVE",
"createdAt": "2025-01-15T10:00:00.000Z",
"updatedAt": "2025-01-15T10:00:00.000Z"
}
],
"count": 1
}
Nota: O campo
secretnunca é incluído nas respostas de listagem. Ele é retornado apenas na criação do webhook (seção 2).
| Campo | Tipo | Descrição |
|---|---|---|
webhookId |
string |
Identificador único do webhook. |
url |
string |
URL de destino das notificações. |
events |
array |
Lista de tipos de evento inscritos. |
status |
string |
ACTIVE ou DISABLED. |
createdAt |
string |
Data/hora de criação (ISO 8601 UTC). |
updatedAt |
string |
Data/hora da última atualização (ISO 8601 UTC). |
Envia um payload de teste para o endpoint do webhook, permitindo verificar se sua integração está funcionando corretamente antes de operar em produção.
POST /v1/webhooks/{webhookId}/test
Authorization: Bearer {access_token}
Nenhum body é necessário.
O SignDocs Brasil envia um payload real ao endpoint cadastrado, contendo:
eventType: TRANSACTION.COMPLETED (simulado)transactionId: tx_test_000000000000000000000000 (ID fictício)test: true (indica que é uma entrega de teste)X-SignDocs-Signature e X-SignDocs-TimestampO payload de teste segue exatamente o mesmo formato e assinatura das entregas reais, permitindo validar toda a cadeia de verificação.
{
"webhookId": "wh_01HX9Z3KQWERTY1234567890",
"testDelivery": {
"httpStatus": 200,
"success": true,
"error": null,
"timestamp": "2025-01-15T14:35:00.000Z"
}
}
| Campo | Descrição |
|---|---|
httpStatus |
Código HTTP retornado pelo seu endpoint (0 se a conexão falhou). |
success |
true se o httpStatus está na faixa 2xx. |
error |
Mensagem de erro, se houver (timeout, conexão recusada, etc). |
timestamp |
Momento da tentativa de entrega. |
curl -X POST "https://api.signdocs.com.br/v1/webhooks/wh_01HX9Z3KQWERTY1234567890/test" \
-H "Authorization: Bearer $TOKEN"
Remove permanentemente um webhook registrado. Após a remoção, nenhuma notificação será mais enviada para a URL associada.
DELETE /v1/webhooks/{webhookId}
Authorization: Bearer {access_token}
Sem corpo na resposta.
| Status | Descrição |
|---|---|
404 |
Webhook não encontrado ou não pertence ao tenant. |
401 |
Token de acesso inválido ou expirado. |
403 |
Token sem escopo webhooks:write. |
curl -X DELETE "https://api.signdocs.com.br/v1/webhooks/wh_01HX9Z3KQWERTY1234567890" \
-H "Authorization: Bearer $TOKEN"
| Possível causa | Solução |
|---|---|
Webhook com status DISABLED |
Verifique via GET /v1/webhooks. Se estiver desativado, recrie ou reative-o. |
| URL inacessível publicamente | A URL deve ser acessível pela internet. Verifique regras de firewall, security groups e DNS. |
| Certificado TLS/SSL inválido | O SignDocs Brasil exige HTTPS com certificado válido. Certificados auto-assinados não são aceitos. |
| Evento não inscrito | Verifique se o tipo de evento desejado está no array events do webhook. |
| Endpoint retornando erro | Verifique os logs do seu servidor. O SignDocs Brasil considera sucesso apenas códigos 2xx. |
| Possível causa | Solução |
|---|---|
| Secret incorreto | Confirme que você está usando o secret retornado na criação do webhook. Ele não pode ser recuperado. |
| Body alterado antes da verificação | Use o body cru (raw bytes) para a verificação, antes de qualquer parsing JSON pelo framework. |
| Encoding incorreto | O payload deve ser tratado como UTF-8. Verifique se não há conversão de encoding. |
| Timestamp expirado (> 300s) | A requisição demorou mais de 5 minutos para chegar. Verifique se há proxies ou filas intermediárias. |
| Secret rotacionado | Se o secret foi rotacionado via painel admin, atualize o secret no seu lado. |
200 OK o mais rápido possível e processe o evento de forma assíncrona (por exemplo, via fila interna).id do payload como chave de idempotência.id em um cache (Redis) ou tabela de controle antes de processar o evento.// Exemplo de idempotência com Redis
async function handleWebhook(event) {
const key = `webhook:${event.id}`;
const alreadyProcessed = await redis.get(key);
if (alreadyProcessed) {
return; // Já processado, ignorar
}
// Processar evento...
await processEvent(event);
// Marcar como processado (TTL de 24 horas)
await redis.set(key, '1', 'EX', 86400);
}
test: truetest: true.app.post('/webhooks/signdocs', (req, res) => {
const event = JSON.parse(req.body);
if (event.test) {
console.log('Entrega de teste recebida com sucesso.');
return res.status(200).send('OK');
}
// Processar evento real...
});
Use o endpoint administrativo GET /admin/tenants/{id}/webhooks/{webhookId}/deliveries (requer autenticação admin) para verificar o histórico de entregas, incluindo status HTTP, número de tentativas e erros.
Antes de ir para produção, confirme os seguintes pontos:
200 OK rapidamente (< 5 segundos).id).test: true) são tratadas separadamente.POST /v1/webhooks/{id}/test) foi chamado com sucesso.