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. 11 eventos suportados, 5 tentativas de entrega em backoff exponencial, verificação criptográfica em 8 linhas de código.
< 5s
Tempo mediano de entrega após o evento
5x
Tentativas de entrega no total, distribuídas por aproximadamente 1 h 20
HMAC-SHA256
Algoritmo de assinatura de cada requisição
Catálogo dos eventos
Os 12 eventos abaixo cobrem o ciclo de vida completo de um envelope Certyneo. Ative aqueles que lhe interessam em Configurações → Webhooks, ignore os outros — a inscrição é granular por evento.
| Evento | Disparo |
|---|---|
envelope.created | Um envelope é criado (por UI, API ou template) — útil para sincronizar um registro do lado do CRM assim que criado. |
envelope.sent | O envelope é enviado aos signatários (primeiro email enviado). Marca o início do ciclo de assinatura ativo. |
envelope.completed | Todos os signatários assinaram e o PDF selado eIDAS é armazenado. O payload contém signedDocumentUrl, um link pré-assinado válido por 7 dias; caso contrário, GET /v1/envelopes/id/signed-document, e a trilha de auditoria via GET /v1/envelopes/id/audit-trail. |
envelope.declined | Um signatário recusou o envelope. O endereço do que recusou está em `data.declinedBy` e o motivo, se foi inserido, em `data.reason`. |
envelope.voided | O envelope foi cancelado pelo emissor antes da assinatura completa. Distinto de `expired` (humano vs timeout). |
envelope.expired | A data de expiração do envelope passou sem assinatura completa. Consulte GET /v1/envelopes/id para saber quais signatários estão faltando. |
envelope.returned_to_sender | Um signatário devolveu o envelope ao emissor para correção, sem recusá-lo. O motivo está em `data.reason` e o autor da devolução em `data.returnedBy`. |
envelope.resubmitted | O emissor corrigiu e reenviou um envelope previamente devolvido. Marca a retomada do ciclo de assinatura. |
recipient.signed | Um signatário individual assinou (mas não necessariamente todos). Útil para acompanhar o progresso e passar para a próxima etapa de um fluxo sequencial. Atenção: o PDF selado não existe ainda nesta fase, nem mesmo para o último signatário — um download iniciado aqui retorna um HTTP 409. Use envelope.completed para o documento. |
recipient.viewed | Um signatário abriu o link de assinatura sem ainda assinar. Útil para relances comerciais direcionadas. |
recipient.approved | Um aprovador validou o envelope sem lhe apor assinatura (fluxo de validação interna). O endereço está em `data.approvedBy`. |
recipient.bounced | O servidor de mensagens de um destinatário recusou definitivamente o convite ou a retomada (caixa inexistente, domínio inativo). O destinatário passa para o status BOUNCED e as retomadas automáticas param. O endereço está em `data.recipientEmail`: corrija-o e reenvie o envelope. |
recipient.signed não significa « documento disponível »
recipient.signed é emitido para CADA signatário, no momento em que ele termina sua assinatura — incluindo o último, antes que o PDF lacrado seja montado e armazenado. Um download acionado a partir deste handler sempre recebe um HTTP 409 « Signed document not available until the envelope is COMPLETED ». Isto não é um erro: é um « ainda não pronto ». Inscreva-se em envelope.completed para recuperar o documento, e mantenha recipient.signed para acompanhar o progresso (quem assinou, e quando).
Formato do payload
Todas as entregas compartilham o mesmo esquema JSON de nível superior: `event`, `data` e `timestamp`. O nome do evento também é repetido no cabeçalho `X-Certyneo-Event`, o que permite rotear antes mesmo de analisar o corpo. O conteúdo de `data` varia de acordo com o evento, mas permanece sempre um objeto plano de valores simples — nunca um array ou objeto aninhado. Aqui está uma entrega `envelope.completed` completa.
POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c{
"id": "cm8f2h6a10004qr9k5p2wm3xt",
"event": "envelope.completed",
"data": {
"envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
"sandbox": false,
"subject": "Contrat de prestation Acme Corp",
"status": "COMPLETED",
"signatureLevel": "ADVANCED",
"aesSignerCount": 2,
"completedAt": "2026-05-27T08:42:13.000Z",
"recipientCount": 2,
"recipients": [
{ "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
{ "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
],
"signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
},
"timestamp": "2026-05-27T08:42:13.521Z"
}signedDocumentUrl é um link pré-assinado válido por 7 dias a contar do evento: evita uma segunda chamada autenticada para recuperar o PDF. Está ausente — e não nulo — se a pré-assinatura falhou, ou em um envelope QES concluído sem documento armazenado; então volte a GET /v1/envelopes/id/signed-document, que permanece a fonte de verdade. Tenha cuidado também com a reprodução manual da dead-letter queue mais de 7 dias após o evento: o link do payload está expirado, o endpoint da API não.
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 de webhook (exibido uma única vez na criação da inscrição). A assinatura é transmitida no cabeçalho `X-Certyneo-Signature`: é o HMAC-SHA256 do corpo bruto da requisição, codificado em hexadecimal, sem prefixo nem timestamp. SEMPRE verifique a assinatura antes de processar o payload — sem esta etapa, qualquer pessoa pode falsificar um evento e chamar seu endpoint.
Node.js / TypeScript
import crypto from "node:crypto";
export function verifyCertyneoSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = Buffer.from(signatureHeader, "hex");
const computed = Buffer.from(expected, "hex");
// timingSafeEqual throws when the two buffers differ in length —
// a malformed header must return false, not crash the handler.
if (received.length !== computed.length) return false;
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(computed, received);
}Python
import hashlib
import hmac
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
`X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
raw request body, computed with your webhook secret.
"""
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)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 demora muito a responder, recusa a conexão ou retorna um 5xx (ou um 429), nós retentamos conforme um backoff exponencial: 5 tentativas no total, ao longo de aproximadamente 1 h 20. Passada a quinta, o evento vai para fila de mensagens não entregues — consultável e rejogável a partir de seu dashboard, mas não mais retentado automaticamente. Uma recusa explícita (401, 403, 404, 410, 422…) nunca é retentada: a resposta não mudaria, o evento vai diretamente para fila de mensagens não entregues.
| Tentativa | Atraso antes da tentativa | Tempo decorrido desde o evento |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 h 21 |
Os prazos indicados são mínimos: a varredura de retentativa é cadenciada por um cron, uma tentativa pode portanto partir ligeiramente após a hora teórica. Os eventos abandonados são listados em Webhooks → Falhas, com um botão de rejogo manual e sem limite de duração de retenção. Um endpoint que encadeia cinco falhas definitivas — ou que responde 404 / 410 — é desativado automaticamente, e você é notificado por email: reative-o uma vez corrigido, o contador recomeça do zero (qualquer entrega bem-sucedida o recoloca também a zero).
Testar sem enviar um envelope verdadeiro
Primeiro crie uma inscrição — a resposta contém o `secret` necessário para a verificação HMAC, exibido uma única vez. A partir de Configurações → Webhooks, o botão « Testar » envia então um POST assinado exatamente como uma entrega real e exibe a resposta bruta do seu servidor: é a forma mais rápida de validar seu handler, localmente via ngrok ou em integração contínua.
# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your.app/webhooks/certyneo",
"events": ["envelope.completed", "recipient.signed"]
}'A URL deve ser publicamente acessível em HTTPS: um endereço privado ou localhost é rejeitado pela proteção SSRF no momento da criação da inscrição.
As assinaturas são separadas por ambiente, assim como as chaves: um webhook criado com uma chave sk_test_ só recebe os eventos dos envelopes de teste, e um criado com uma chave sk_live_ só os dos envelopes reais. Assim, um envelope de teste nunca chega à sua URL de produção, e cada payload indica "sandbox" (true ou false).
6 práticas a serem observadas
- Verificar a assinatura HMAC ANTES de qualquer leitura do body — usar uma comparação timing-safe.
- Desduplicar no cabeçalho `X-Certyneo-Delivery-Id` (idêntico a `id` no corpo) armazenando os identificadores já vistos na base — uma reprodução pode entregar novamente um evento que já processou se seu 2xx foi perdido, com o mesmo identificador em cada tentativa.
- Responda HTTP 2xx em no máximo 10 segundos, depois processe de forma assíncrona (fila). Além disso, a entrega é cortada e contada como um fracasso.
- 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.
- Monitorar a página Webhooks → Falhas: você é notificado por email se seu endpoint é desativado, mas não evento por evento — um evento que esgota suas tentativas, cabe a você ir rejogá-lo.
- Filtre os eventos na inscrição em vez de no seu handler, e permaneça sob o limite de 5 inscrições por conta.
Não é obrigado a alojar um ponto final para receber estes eventos: o conector coloca a subscrição por si e inicia o fluxo diretamente num evento de envelope. Ver Certyneo para Power Automate e Microsoft 365.
Para saber mais
Pronto para conectar seus sistemas?
Webhooks e API REST são inclusos a partir do plano Standard. Crie sua conta, gere uma chave `sk_test_` e conecte seu endpoint em sandbox antes de passar para produção.