Webhooks — ricevi gli eventi di firma in tempo reale
Configurate un URL HTTPS nel vostro dashboard Certyneo e ricevete un POST firmato HMAC-SHA256 non appena si verifica un evento sulle vostre buste: firma, rifiuto, scadenza. 11 eventi supportati, 5 tentativi di consegna in backoff esponenziale, verifica crittografica in 8 righe di codice.
< 5s
Tempo mediano di consegna dopo l'evento
5x
Tentativi di consegna in totale, distribuiti su circa 1 h 20
HMAC-SHA256
Algoritmo di firma di ogni richiesta
Catalogo degli eventi
Gli 12 eventi qui sotto coprono l'integrità del ciclo di vita di una busta Certyneo. Attivate quelli che vi interessano in Impostazioni → Webhook, ignorate 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 e il PDF sigillato eIDAS è archiviato. Il payload contiene signedDocumentUrl, un collegamento pre-firmato valido 7 giorni; in caso contrario, GET /v1/envelopes/id/signed-document, e la pista di audit tramite GET /v1/envelopes/id/audit-trail. |
envelope.declined | Un firmatario ha rifiutato la busta. L'indirizzo del rifiutante è in `data.declinedBy` e il motivo, se è stato inserito, in `data.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. Interroga GET /v1/envelopes/id per conoscere i firmatari mancanti. |
envelope.returned_to_sender | Un firmatario ha rinviato la busta all'emittente per correzione, senza rifiutarla. Il motivo si trova in `data.reason` e l'autore del rinvio in `data.returnedBy`. |
envelope.resubmitted | L'emittente ha corretto e quindi rinviato una busta precedentemente restituita. Segna la ripresa del ciclo di firma. |
recipient.signed | Un firmatario individuale ha firmato (ma non necessariamente tutti). Utile per seguire l'avanzamento e procedere al passaggio successivo di un workflow sequenziale. Attenzione: il PDF sigillato non esiste ancora in questa fase, nemmeno per l'ultimo firmatario — un download avviato qui restituisce un HTTP 409. Utilizza envelope.completed per il documento. |
recipient.viewed | Un firmatario ha aperto il link di firma senza ancora firmare. Utile per i follow-up commerciali mirati. |
recipient.approved | Un approvatore ha convalidato la busta senza apporre una firma (workflow di convalida interna). L'indirizzo si trova in `data.approvedBy`. |
recipient.bounced | Il server di messaggistica di un destinatario ha rifiutato definitivamente l'invito o il tentativo (casella inesistente, dominio inattivo). Il destinatario passa allo stato BOUNCED e i tentativi automatici si arrestano. L'indirizzo si trova in `data.recipientEmail`: correggerlo e rinviare la busta. |
recipient.signed non significa « documento disponibile »
recipient.signed è emesso per OGNI firmatario, nel momento in cui completa la firma — incluso l'ultimo, prima che il PDF sigillato sia assemblato e archiviato. Un download attivato da questo gestore riceve quindi sempre un HTTP 409 « Signed document not available until the envelope is COMPLETED ». Non è un errore: è un « non ancora pronto ». Iscriviti a envelope.completed per recuperare il documento e mantieni recipient.signed per seguire l'avanzamento (chi ha firmato e quando).
Formato del payload
Tutte le consegne condividono lo stesso schema JSON di primo livello: `event`, `data` e `timestamp`. Il nome dell'evento è anche ripetuto nell'intestazione `X-Certyneo-Event`, il che consente il routing anche prima di analizzare il corpo. Il contenuto di `data` varia a seconda dell'evento, ma rimane sempre un oggetto piatto di valori semplici — mai un array né un oggetto annidato. Ecco una consegna `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 è un link pre-firmato valido per 7 giorni a partire dall'evento: evita una seconda chiamata autenticata per recuperare il PDF. È assente — e non null — se la pre-firma non ha avuto esito positivo, o su una busta QES completata senza documento archiviato; torna allora a GET /v1/envelopes/id/signed-document, che rimane la fonte di verità. Fai attenzione anche alla riproduzione manuale dalla dead-letter queue più di 7 giorni dopo l'evento: il link nel payload è scaduto, l'endpoint API no.
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
Ogni richiesta è firmata con il tuo segreto webhook (visualizzato una sola volta al momento della creazione della sottoscrizione). La firma è trasmessa nell'intestazione `X-Certyneo-Signature`: è l'HMAC-SHA256 del corpo grezzo della richiesta, codificato in esadecimale, senza prefisso né timestamp. Verifica SEMPRE la firma prima di elaborare il payload — senza questo passaggio, chiunque può falsificare un evento e chiamare il tuo 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)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 impiega troppo tempo a rispondere, rifiuta la connessione o restituisce un 5xx (o un 429), ritentiamo secondo un backoff esponenziale: 5 tentativi in totale, in circa 1 h 20. Dopo il quinto, l'evento finisce in dead-letter queue — consultabile e riproducibile dal vostro dashboard, ma non più ritentato automaticamente. Un rifiuto esplicito (401, 403, 404, 410, 422…) non è invece mai ritentato: la risposta non cambierebbe, l'evento finisce direttamente in dead-letter queue.
| Tentativo | Ritardo prima del tentativo | Tempo trascorso dall'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 |
I tempi indicati sono minimi: la scansione di retry è cadenzata da un cron, quindi un tentativo può partire leggermente dopo l'ora teorica. Gli eventi abbandonati sono elencati in Webhooks → Errori, con un pulsante di riproduzione manuale e senza limite di durata di conservazione. Un endpoint che accumula cinque errori definitivi — o che risponde 404 / 410 — viene disabilitato automaticamente, e ne siete avvertiti via email: riabilitatelo una volta corretto, il contatore riparte da zero (qualsiasi consegna riuscita lo ripristina a zero).
Testare senza inviare una vera busta
Per prima cosa crea una sottoscrizione — la risposta contiene il `secret` necessario per la verifica HMAC, visualizzato una sola volta. Da Impostazioni → Webhooks, il pulsante « Test » invia quindi un POST firmato esattamente come una consegna reale e ti mostra la risposta grezza del tuo server: è il modo più veloce per convalidare il tuo gestore, localmente via ngrok o in integrazione continua.
# 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"]
}'L'URL deve essere raggiungibile pubblicamente in HTTPS: un indirizzo privato o localhost viene rifiutato dalla protezione SSRF al momento della creazione della sottoscrizione.
Gli abbonamenti sono separati per ambiente, come le chiavi: un webhook creato con una chiave sk_test_ riceve solo gli eventi delle buste di prova, uno creato con una chiave sk_live_ solo quelli delle buste reali. Una busta di prova non raggiunge quindi mai l'URL di produzione, e ogni payload indica «sandbox» (true o false).
6 pratiche da rispettare
- Verificare la firma HMAC PRIMA di qualsiasi lettura del body — utilizzare un confronto timing-safe.
- Deduplica sull'intestazione `X-Certyneo-Delivery-Id` (identica a `id` nel corpo) memorizzando gli identificatori già visti in base — una riesecuzione può rierogare un evento che hai già elaborato se il tuo 2xx si è perso, con lo stesso identificatore ad ogni tentativo.
- Rispondi HTTP 2xx entro 10 secondi massimo, quindi elabora in modo asincrono (coda). Oltre ciò, la consegna viene interrotta e conteggiata come un errore.
- Registrare il body grezzo + la firma completa in debug — la verifica HMAC spesso fallisce su un BOM o uno spazio invisibile.
- Monitorare la pagina Webhooks → Errori: siete avvertiti via email se il vostro endpoint è disabilitato, ma non evento per evento — un evento che esaurisce i suoi tentativi, tocca a voi andarlo a riprodurre.
- Filtra gli eventi alla sottoscrizione piuttosto che nel tuo gestore e rimani sotto il limite di 5 sottoscrizioni per account.
Non sei obbligato a ospitare un endpoint per ricevere questi eventi: il connettore pone l'abbonamento per te e avvia il flusso direttamente su un evento di busta. Vedi Certyneo per Power Automate e Microsoft 365.
Per approfondire
Pronto a connettere i vostri sistemi?
I webhook e l'API REST sono inclusi a partire dal piano Standard. Crea il tuo account, genera una chiave `sk_test_` e collega il tuo endpoint in sandbox prima di passare in produzione.