Webhooks — ricevi gli eventi di firma in tempo reale
Configura un URL HTTPS nel tuo dashboard Certyneo e ricevi un POST firmato HMAC-SHA256 non appena si verifica un evento sulle tue buste: firma, rifiuto, scadenza. 8 eventi supportati, retry esponenziale su 6 tentativi, verifica crittografica in 8 righe di codice.
< 5s
Tempo mediano di consegna dopo l'evento
5x
Tentativi in caso di errore (fino a ~9 ore dopo)
HMAC-SHA256
Algoritmo di firma di ogni richiesta
Catalogo degli eventi
Gli 8 eventi qui sotto coprono l'intero ciclo di vita di una busta Certyneo. Attiva quelli che ti interessano in Impostazioni → Webhooks, ignora gli altri — l'iscrizione è granulare per evento.
| Evento | Attivazione |
|---|---|
envelope.created | Una busta viene creata (tramite UI, API o template) — utile per sincronizzare un record nel CRM al momento della creazione. |
envelope.sent | La busta viene inviata ai firmatari (primo email inviato). Segna l'inizio del ciclo di firma attivo. |
envelope.completed | Tutti i firmatari hanno firmato. Il PDF sigillato eIDAS e l'audit trail sono disponibili tramite `proof_pdf_url` nel payload. |
envelope.declined | Un firmatario ha rifiutato la busta. Il motivo del rifiuto (se fornito dal firmatario) è in `data.decline_reason`. |
envelope.voided | La busta è stata annullata dall'emittente prima della firma completa. Distinto da `expired` (umano vs timeout). |
envelope.expired | La data di scadenza della busta è passata senza firma completa. L'elenco dei firmatari mancanti è in `data.missing_signers`. |
recipient.signed | Un firmatario individuale ha firmato (ma non necessariamente tutti). Utile per i workflow sequenziali: attivare il passaggio al firmatario successivo. |
recipient.viewed | Un firmatario ha aperto il link di firma senza ancora firmare. Utile per i follow-up commerciali mirati. |
Formato del payload
Tutti gli eventi condividono lo stesso schema JSON di livello superiore: `event`, `envelope_id`, `occurred_at`, `data`. Il contenuto di `data` varia a seconda dell'evento (campi documentati per evento nel riferimento API). Ecco un esempio completo di `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"
}
}Il payload è codificato UTF-8, senza BOM. La firma HMAC è calcolata sul body grezzo così come inviato — non alterate gli spazi, il re-parsing JSON spesso modifica l'ordine delle chiavi e rompe la verifica.
Verificare la firma HMAC
developers.webhooks.signature.lead
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)Errore frequente
NON utilizzate `===` o `==` per confrontare la firma attesa con quella ricevuta. Utilizzate una funzione timing-safe (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python). Senza questo, la differenza di tempo di confronto tra due firme rivela progressivamente il segreto a un attaccante paziente (timing attack).
Politica di retry
Se il vostro endpoint risponde diversamente da HTTP 2xx (timeout, 5xx, connection refused), effettuiamo nuovi tentativi secondo un backoff esponenziale. Un totale di 6 tentativi in ~9 ore. Dopo i 6 tentativi, l'evento è contrassegnato come `failed` nel vostro dashboard webhook e viene inviato un email di avviso.
| Tentativo | Ritardo cumulativo | Tempo trascorso |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 30 min | 36 min |
| #5 | + 2 h | 2h 36 |
| #6 | + 6 h | 8h 36 |
Se desiderate retransmettere manualmente un evento non riuscito, il dashboard webhook offre un pulsante « Re-deliver » su ogni consegna fallita — disponibile per 7 giorni dopo il fallimento definitivo.
Testare senza inviare una vera busta
L'endpoint `/v1/webhooks/test` accetta un URL e un tipo di evento, e POSTa un payload fittizio firmato esattamente come uno vero. Ideale per convalidare il vostro handler nell'integrazione continua o durante lo sviluppo locale (con ngrok o 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 pratiche da rispettare
- Verificare la firma HMAC PRIMA di qualsiasi lettura del body — utilizzare un confronto timing-safe.
- Elaborare ogni `envelope_id` × `event` una sola volta memorizzando gli ID già visti nel database (i retry possono re-consegnare lo stesso evento).
- Rispondere HTTP 200 entro massimo 5 secondi, poi elaborare in modo asincrono (queue). Altrimenti andrete in timeout e sarete conteggiati come retry.
- Registrare il body grezzo + la firma completa in debug — la verifica HMAC spesso fallisce su un BOM o uno spazio invisibile.
- Collegare una dead-letter queue agli eventi `failed` per retransmettere manualmente in caso di incidente dal lato del vostro servizio.
- Tollerare uno scostamento di 5 minuti tra `t=` e l'ora di ricezione — oltre, rifiutare per bloccare i replay.
Per approfondire
Pronto a connettere i vostri sistemi?
Create un account gratuito in 2 minuti — la configurazione webhook è disponibile già dal piano Starter (gratuito, 5 buste/mese).