Ir para o conteúdo principal
Certyneo
Documentação para desenvolvedores

Webhooks — receba os eventos de assinatura em tempo real

Configure uma URL HTTPS no seu dashboard Certyneo e receba um POST assinado HMAC-SHA256 assim que um evento ocorrer em seus envelopes: assinatura, recusa, expiração. 8 eventos suportados, retry exponencial em 6 tentativas, verificação criptográfica em 8 linhas de código.

< 5s

Tempo mediano de entrega após o evento

5x

Tentativas em caso de falha (até ~9h depois)

HMAC-SHA256

Algoritmo de assinatura de cada requisição

Catálogo dos eventos

Os 8 eventos abaixo cobrem a totalidade do ciclo de vida de um envelope Certyneo. Ative aqueles que o interessam em Parâmetros → Webhooks, ignore os outros — a assinatura é granular por evento.

EventoDisparo
envelope.createdUm envelope é criado (por UI, API ou template) — útil para sincronizar um registro do lado do CRM assim que criado.
envelope.sentO envelope é enviado aos signatários (primeiro email enviado). Marca o início do ciclo de assinatura ativo.
envelope.completedTodos os signatários assinaram. O PDF selado eIDAS e o audit trail estão disponíveis via `proof_pdf_url` no payload.
envelope.declinedUm signatário recusou o envelope. O motivo da recusa (se fornecido pelo signatário) está em `data.decline_reason`.
envelope.voidedO envelope foi cancelado pelo emissor antes da assinatura completa. Distinto de `expired` (humano vs timeout).
envelope.expiredA data de expiração do envelope passou sem assinatura completa. A lista dos signatários faltantes está em `data.missing_signers`.
recipient.signedUm signatário individual assinou (mas não necessariamente todos). Útil para workflows sequenciais: disparar a passagem para o próximo signatário.
recipient.viewedUm signatário abriu o link de assinatura sem ainda assinar. Útil para relances comerciais direcionadas.

Formato do payload

Todos os eventos compartilham o mesmo schema JSON de nível superior: `event`, `envelope_id`, `occurred_at`, `data`. O conteúdo de `data` varia conforme o evento (campos documentados por evento na referência da API). Eis um exemplo completo de `envelope.completed`.

{
  "event": "envelope.completed",
  "envelope_id": "env_01HG3K8X9Y2N4P5Q6R7S8T9V0W",
  "occurred_at": "2026-05-27T08:42:13.521Z",
  "data": {
    "title": "Contrat de prestation Acme Corp",
    "status": "completed",
    "created_at": "2026-05-24T14:12:00.000Z",
    "completed_at": "2026-05-27T08:42:13.000Z",
    "signers": [
      {
        "email": "client@acme.example",
        "name": "Alex Client",
        "signed_at": "2026-05-27T08:42:13.000Z",
        "method": "eidas_aes",
        "ip": "203.0.113.42"
      }
    ],
    "proof_pdf_url": "https://certyneo.com/api/envelopes/env_01HG.../proof.pdf"
  }
}

O payload é codificado em UTF-8, sem BOM. A assinatura HMAC é calculada sobre o body bruto tal como enviado — não altere espaços, o re-parsing JSON geralmente modifica a ordem das chaves e interrompe a verificação.

Verificar a assinatura HMAC

Cada requisição é assinada com seu segredo webhook (visível apenas uma vez durante a criação no dashboard, depois criptografado em repouso). A assinatura é transmitida no cabeçalho `Certyneo-Signature` no formato `t=<timestamp>,v1=<hex_hmac>`. SEMPRE verifique a assinatura antes de processar o payload — sem esta etapa, qualquer pessoa pode forjar um evento e chamar seu endpoint.

Node.js / TypeScript

import crypto from "node:crypto";

export function verifyCertyneoSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  // Header format: t=<timestamp>,v1=<hex_hmac>
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=")),
  );
  const ts = parts.t;
  const sig = parts.v1;
  if (!ts || !sig) return false;

  // Reject events older than 5 minutes (replay protection).
  const age = Math.abs(Date.now() / 1000 - Number(ts));
  if (age > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  // timingSafeEqual to mitigate timing attacks.
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(sig, "hex"),
  );
}

Python

import hashlib
import hmac
import time


def verify_certyneo_signature(
    raw_body: bytes, signature_header: str, secret: str
) -> bool:
    """Verify a Certyneo webhook signature.

    Header format: `t=<timestamp>,v1=<hex_hmac>`
    Rejects events older than 5 minutes (replay protection).
    """
    parts = dict(p.split("=") for p in signature_header.split(","))
    ts = parts.get("t")
    sig = parts.get("v1")
    if not ts or not sig:
        return False

    if abs(time.time() - int(ts)) > 300:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, sig)

Erro frequente

NÃO use `===` ou `==` para comparar a assinatura esperada com a recebida. Use uma função timing-safe (`crypto.timingSafeEqual` em Node, `hmac.compare_digest` em Python). Sem isso, a diferença de tempo de comparação entre duas assinaturas revela progressivamente o segredo a um atacante paciente (timing attack).

Política de retry

Se seu endpoint responder algo diferente de HTTP 2xx (timeout, 5xx, conexão recusada), faremos novas tentativas conforme um backoff exponencial. No total 6 tentativas em ~9 horas. Após as 6, o evento é marcado como `failed` em seu dashboard webhook e um email de alerta é enviado.

TentativaAtraso cumulativoTempo decorrido
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 30 min36 min
#5+ 2 h2h 36
#6+ 6 h8h 36

Se desejar retransmitir manualmente um evento falhado, o dashboard webhook oferece um botão «Re-deliver» em cada entrega malsucedida — disponível durante 7 dias após a falha definitiva.

Testar sem enviar um envelope verdadeiro

O endpoint `/v1/webhooks/test` aceita uma URL e um tipo de evento, e POSTa um payload fictício assinado exatamente como um verdadeiro. Ideal para validar seu handler em integração contínua ou durante o desenvolvimento local (com ngrok ou equivalente).

# Trigger a test webhook to your endpoint
curl -X POST https://api.certyneo.com/v1/webhooks/test \
  -H "Authorization: Bearer $CERTYNEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event": "envelope.completed", "url": "https://your.app/webhooks/certyneo"}'

6 práticas a serem observadas

  • Verificar a assinatura HMAC ANTES de qualquer leitura do body — usar uma comparação timing-safe.
  • Processar cada `envelope_id` × `event` uma única vez armazenando os IDs já vistos no banco (os retries podem reentrega do mesmo evento).
  • Responder HTTP 200 em no máximo 5 segundos, depois processar de forma assíncrona (fila). Caso contrário, você sofrerá timeout e será contado como retry.
  • Registrar o body bruto + a assinatura completa em debug — a verificação HMAC geralmente falha em um BOM ou um espaço em branco invisível.
  • Conectar uma dead-letter queue aos eventos `failed` para retransmitir manualmente em caso de incidente do seu lado do serviço.
  • Tolerar um desvio de 5 minutos entre `t=` e a hora de recebimento — além disso, rejeitar para bloquear replays.

Para ir mais longe

Pronto para conectar seus sistemas?

Crie uma conta gratuita em 2 minutos — a configuração webhook está disponível a partir do plano Starter (gratuito, 5 envelopes/mês).