Guias de Desenvolvimento

Signing Sessions

Guias dos SDKs

Guia de Integração com Webhooks — SignDocs Brasil

Sumário

  1. Visão geral
  2. Registrar webhook
  3. Tipos de evento
  4. Formato do payload
  5. Verificação de assinatura HMAC-SHA256
  6. Política de retry
  7. Listar webhooks
  8. Testar webhook
  9. Remover webhook
  10. Troubleshooting

1. Visão geral

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.


2. Registrar webhook

Para começar a receber notificações, registre uma URL de webhook com os tipos de evento desejados.

Requisição

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"
  ]
}

Validações

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.

Resposta (201 Created)

{
  "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.

Escopo necessário

O token de acesso deve conter o escopo webhooks:write.


3. Tipos de evento

O SignDocs Brasil emite 11 tipos de evento, divididos em quatro categorias.

Eventos de transação

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.

Eventos de etapa (step)

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).

Eventos de cota

Tipo Descrição
QUOTA.WARNING A cota de transações está próxima do limite. Disparado em 80%, 90% e 100% de utilização.

Eventos de API

Tipo Descrição
API.DEPRECATION_NOTICE Aviso de deprecação de endpoint ou versão da API. Planeje a migração.

Selecionando eventos

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.


4. Formato do payload

Todas as entregas de webhook seguem a mesma estrutura JSON de envelope. O campo data varia conforme o tipo de evento.

Estrutura do envelope

{
  "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).

Exemplo: TRANSACTION.COMPLETED

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"
  }
}

Exemplo: TRANSACTION.FAILED

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
    }
  }
}

Exemplo: STEP.COMPLETED

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
  }
}

5. Verificação de assinatura HMAC-SHA256

Cada entrega de webhook inclui dois headers que permitem verificar a autenticidade e a integridade do payload.

Headers enviados

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.

Como a assinatura é gerada

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

Passos para verificação

  1. Extrair os headers X-SignDocs-Signature e X-SignDocs-Timestamp da requisição.
  2. Validar a frescura do timestamp: calcular |agora - timestamp| e rejeitar se a diferença for superior a 300 segundos (5 minutos). Isso previne ataques de replay.
  3. Recalcular a assinatura: montar timestamp + "." + body_cru e gerar o HMAC-SHA256 com o secret armazenado.
  4. Comparar as assinaturas de forma segura (timing-safe comparison). Se forem iguais, o payload é autêntico.

Exemplo em Node.js

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);
//   }
// });

Exemplo em Python

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.


6. Política de retry

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.

Comportamento

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

Idempotência

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.

Desativação automática

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.


7. Listar webhooks

Retorna todos os webhooks registrados para o tenant autenticado.

Requisição

GET /v1/webhooks
Authorization: Bearer {access_token}

Resposta (200 OK)

{
  "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 secret nunca é incluído nas respostas de listagem. Ele é retornado apenas na criação do webhook (seção 2).

Campos da resposta

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).

8. Testar webhook

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.

Requisição

POST /v1/webhooks/{webhookId}/test
Authorization: Bearer {access_token}

Nenhum body é necessário.

Comportamento

O SignDocs Brasil envia um payload real ao endpoint cadastrado, contendo:

O payload de teste segue exatamente o mesmo formato e assinatura das entregas reais, permitindo validar toda a cadeia de verificação.

Resposta (200 OK)

{
  "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.

Exemplo de uso

curl -X POST "https://api.signdocs.com.br/v1/webhooks/wh_01HX9Z3KQWERTY1234567890/test" \
  -H "Authorization: Bearer $TOKEN"

9. Remover webhook

Remove permanentemente um webhook registrado. Após a remoção, nenhuma notificação será mais enviada para a URL associada.

Requisição

DELETE /v1/webhooks/{webhookId}
Authorization: Bearer {access_token}

Resposta (204 No Content)

Sem corpo na resposta.

Erros possíveis

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.

Exemplo de uso

curl -X DELETE "https://api.signdocs.com.br/v1/webhooks/wh_01HX9Z3KQWERTY1234567890" \
  -H "Authorization: Bearer $TOKEN"

10. Troubleshooting

O webhook não está recebendo notificações

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.

Assinatura HMAC inválida

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.

Erros de timeout no endpoint

Entregas duplicadas

// 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);
}

Payload de teste tem test: 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...
});

Consultar histórico de entregas

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.

Checklist de integração

Antes de ir para produção, confirme os seguintes pontos: