Vai al contenuto principale
Certyneo
Documentazione sviluppatore

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.

EventoAttivazione
envelope.createdUna busta viene creata (tramite UI, API o template) — utile per sincronizzare un record nel CRM al momento della creazione.
envelope.sentLa busta viene inviata ai firmatari (primo email inviato). Segna l'inizio del ciclo di firma attivo.
envelope.completedTutti i firmatari hanno firmato. Il PDF sigillato eIDAS e l'audit trail sono disponibili tramite `proof_pdf_url` nel payload.
envelope.declinedUn firmatario ha rifiutato la busta. Il motivo del rifiuto (se fornito dal firmatario) è in `data.decline_reason`.
envelope.voidedLa busta è stata annullata dall'emittente prima della firma completa. Distinto da `expired` (umano vs timeout).
envelope.expiredLa data di scadenza della busta è passata senza firma completa. L'elenco dei firmatari mancanti è in `data.missing_signers`.
recipient.signedUn firmatario individuale ha firmato (ma non necessariamente tutti). Utile per i workflow sequenziali: attivare il passaggio al firmatario successivo.
recipient.viewedUn 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.

TentativoRitardo cumulativoTempo trascorso
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 30 min36 min
#5+ 2 h2h 36
#6+ 6 h8h 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).