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.
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",
"STEP.COMPLETED"
]
}
| 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",
"STEP.COMPLETED"
],
"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 SignDocs Brasil emite 11 tipos de evento, divididos em quatro categorias.
| 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 |
A transação falhou definitivamente (por exemplo, número máximo de tentativas excedido). |
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 (fallback) após falha na política primária. |
| Tipo | Descrição |
|---|---|
STEP.STARTED |
Uma etapa de verificação foi iniciada pelo signatário. |
STEP.COMPLETED |
Uma etapa foi concluída com sucesso. |
STEP.FAILED |
Uma etapa falhou (por exemplo, falha na verificação de liveness). |
| Tipo | Descrição |
|---|---|
QUOTA.WARNING |
A cota de transações está próxima do limite. Disparado em 80%, 90% e 100% de utilização. |
| Tipo | Descrição |
|---|---|
API.DEPRECATION_NOTICE |
Aviso de deprecação de endpoint ou versão da API. Planeje a migração. |
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 transação falha definitivamente (por exemplo, o signatário excedeu o número máximo de tentativas de liveness).
{
"id": "del_01HX9Z5PQRSTU2345678901",
"eventType": "TRANSACTION.FAILED",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"timestamp": "2025-01-15T15:10:00.000Z",
"data": {
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"status": "FAILED",
"reason": "MAX_ATTEMPTS_EXCEEDED",
"failedStep": {
"stepId": "step_01HX9Z3LABCDE1234567890",
"type": "LIVENESS",
"attempts": 3
}
}
}
Disparado quando uma etapa individual é concluída com sucesso.
{
"id": "del_01HX9Z6QVWXYZ3456789012",
"eventType": "STEP.COMPLETED",
"tenantId": "tenant_01HW8X2JQWERTY0987654321",
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"timestamp": "2025-01-15T14:25:00.000Z",
"data": {
"transactionId": "tx_01HX9Z3KQWERTY1234567890",
"stepId": "step_01HX9Z3LABCDE1234567890",
"stepType": "LIVENESS",
"status": "COMPLETED",
"confidence": 99.7
}
}
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",
"STEP.COMPLETED"
],
"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.