Vai al contenuto principale
Approvazione specifica delle clausole vessatorie ex art. 1341 c.c. — servizio per imprese (B2B)

Documentazione

Tutto ciò che serve per integrare Convalido nel tuo sito o nella tua applicazione: dal primo test in cinque minuti al riferimento completo di widget, API e pacchetto probatorio.

1. Panoramica

Convalido raccoglie l'approvazione specifica per iscritto delle clausole vessatorie (art. 1341, comma 2, c.c.) nei contratti conclusi online tra imprese, sostituendo la casella da spuntare con una firma elettronica semplice: un codice OTP inviato via SMS o email e inserito dal firmatario. Per ogni clausola approvata produce un'evidenza immutabile, marcata temporalmente (RFC 3161) e conservata dieci anni.

L'integrazione ha due parti, entrambe obbligatorie:

  1. Lato pagina — il widget JavaScript, che sostituisce il checkbox sotto le clausole e gestisce invio e verifica del codice.
  2. Lato server — una chiamata dal tuo backend che conferma il token prodotto dal widget prima di concludere l'ordine.

Indirizzi: API https://api.convalido.it · dashboard https://account.convalido.it · piattaforma di test https://test.convalido.it. Tutte le risposte sono JSON in UTF-8, tutte le date sono UTC in formato ISO 8601.

2. Guida rapida (cinque minuti)

  1. Crea l'account su account → Registrazione: ottieni la dashboard e le approvazioni gratuite di prova. Conferma l'email.
  2. In Impostazioni aggiungi i domini dei siti che useranno il widget e genera una chiave API di test (cv_test_…): le approvazioni fatte con una chiave di test non si pagano e sono marcate come test.
  3. Nel modulo contrattuale, sotto la clausola vessatoria, inserisci il widget:
    <div id="clausola-foro">Art. 12 — Foro competente. …</div>
    <div data-convalido data-api-key="cv_test_LA_TUA_CHIAVE" data-clause-id="foro-competente"
         data-contract-id="ORD-2026-000123" data-clause-selector="#clausola-foro"></div>
    <script src="https://api.convalido.it/widget.js" async></script>
  4. Prova il flusso: inserisci la tua email, ricevi il codice, confermalo. L'approvazione compare nella dashboard con il badge test.
  5. Nel backend, prima di concludere l'ordine, verifica il token che il widget ha aggiunto al form.
  6. Quando sei pronto, genera una chiave live (cv_live_…), sostituiscila nel widget e nel backend, e attiva un piano da Abbonamento.

Se vuoi vedere il flusso prima ancora di registrarti, usa la piattaforma di test (sezione 10).

3. Concetti chiave

ClausolaIl testo che il firmatario approva. È identificato da un clause_id stabile (es. foro-competente) e dal suo testo integrale, letto dalla pagina al momento dell'invio. Convalido conserva il testo e la sua impronta SHA-256.
ContrattoIl contract_id è il riferimento all'ordine o al contratto nel tuo sistema. Più clausole dello stesso contratto condividono lo stesso contract_id.
TransazioneUna richiesta di codice per una clausola: nasce pending, diventa verified con il codice giusto, oppure expired, failed (tentativi esauriti), clause_mismatch (testo cambiato) o delivery_failed.
Approvazione singolaUn widget sotto ogni clausola, un codice per ciascuna. I widget della stessa pagina condividono il contatto già inserito.
Approvazione in gruppoUn solo widget elenca più clausole del contratto (marcate con data-convalido-clause) e un solo codice le approva tutte, ciascuna singolarmente: una transazione e un'evidenza per clausola, con l'elenco delle altre clausole coperte dallo stesso codice. È l'equivalente della seconda firma cartacea che richiama le clausole numerate. Singole e gruppi possono convivere nello stesso contratto.
EvidenzaIl file immutabile prodotto per ogni clausola verificata (sezione 9), identificato da un evidence_id.
Token di esitoUn JWT firmato da Convalido, valido 15 minuti, che il widget consegna al tuo form. Il tuo server lo verifica con la propria chiave API prima di considerare la clausola approvata.
Chiavi APIcv_live_… per la produzione, cv_test_… per le prove. Sono legate ai domini autorizzati e si gestiscono dalla dashboard (fino a 5 attive; il valore compare una sola volta).

4. Widget

Vanilla JavaScript, ~9 KB, nessuna dipendenza, interfaccia isolata in Shadow DOM (il tuo CSS non interferisce e viceversa). Non usa cookie né localStorage: tutto lo stato vive sul server. Il testo delle clausole viene riletto dalla pagina a ogni invio: se al momento della verifica differisce da quello per cui il codice era stato generato, il server rifiuta l'approvazione.

4.1 Approvazione singola

<div id="clausola-foro">Art. 12 — Foro competente. Per qualsiasi controversia …</div>

<div data-convalido
     data-api-key="cv_live_LA_TUA_CHIAVE"
     data-clause-id="foro-competente"
     data-contract-id="ORD-2026-000123"
     data-clause-selector="#clausola-foro"
     data-contract-selector="#contratto"          <!-- facoltativo: testo integrale del contratto -->
     data-contract-version="CGF-2026-08"          <!-- facoltativo: versione delle condizioni -->
     data-account-id="CLIENTE-42"></div>

<script src="https://api.convalido.it/widget.js" async></script>

4.2 Approvazione in gruppo

<div data-convalido-clause="foro-competente">Art. 12 — Foro competente. …</div>
<div data-convalido-clause="rinnovo-tacito">Art. 15 — Rinnovo tacito. …</div>
<div data-convalido-clause="decadenza-reclami">Art. 18 — Decadenza. …</div>

<div data-convalido
     data-api-key="cv_live_LA_TUA_CHIAVE"
     data-contract-id="ORD-2026-000123"
     data-clause-selector="[data-convalido-clause]"></div>

Il widget rileva da solo il modo: se il selettore trova più elementi con data-convalido-clause lavora in gruppo (riporta al firmatario il testo integrale di ogni clausola e chiede un solo codice), altrimenti in modo singolo. In entrambi i casi ciò che viene inviato, confrontato e conservato è sempre l'intero testo della clausola così com'è nella pagina — mai solo il titolo o un riassunto: per questo il selettore deve puntare all'elemento che contiene la clausola completa.

4.3 Attributi

AttributoObbligatorioSignificato
data-api-keyChiave API. È visibile nel sorgente: per questo le chiamate sono accettate solo dai domini autorizzati (header Origin).
data-contract-idRiferimento all'ordine/contratto (max 128 caratteri).
data-clause-selectorsì*Selettore CSS dell'elemento (o degli elementi) con il testo. In alternativa data-clause-text con il testo esplicito (solo modo singolo).
data-clause-idsingolaIdentificativo stabile della clausola. Nel gruppo l'id di ogni clausola è il valore di data-convalido-clause.
data-contract-selectorno (consigliato)Selettore CSS dell'elemento che contiene il testo integrale del contratto (condizioni generali con dentro le clausole). Viene riletto a ogni invio come le clausole, confrontato via impronta tra richiesta e verifica (contract_mismatch se cambia) e conservato per intero nel pacchetto probatorio, separatamente dalle clausole vessatorie: è la "versione delle condizioni" che la Cassazione chiede di poter provare.
data-contract-versionnoEtichetta di versione delle condizioni (max 64 caratteri, es. CGF-2026-08), registrata accanto a contract_id nel pacchetto e nell'API.
data-account-idnoIl tuo identificativo del cliente: permette di cercare in dashboard tutte le approvazioni di un cliente.
data-input-namenoNome base dei campi nascosti aggiunti al form (default convalido_evidence_token).
data-debugnoEmette l'evento convalido:request con richiesta/risposta di ogni chiamata (chiave troncata, codice mascherato). Usato dalla piattaforma di test.

4.4 Cosa succede a verifica avvenuta

Il widget mostra "Clausola approvata specificamente" con l'identificativo dell'evidenza e la marca temporale, imposta data-convalido-verified sull'elemento host e, se si trova dentro un <form>, aggiunge i campi nascosti:

  • convalido_evidence_token — il token di esito (della prima clausola, nel gruppo);
  • convalido_evidence_token_evidence_id — l'evidence_id;
  • convalido_evidence_token_all — JSON con clause_id, evidence_id, evidence_token di ogni clausola (utile nel gruppo).

4.5 Eventi

Tutti emessi sull'elemento host, con bubbling, quindi intercettabili anche su document:

convalido:sentcodice inviato; detail = risposta di /v1/otp/request
convalido:verifiedapprovazione riuscita; detail = risposta di /v1/otp/verify (con evidences[])
convalido:errordetail = {code, message, payload}
convalido:requestsolo con data-debug: {path, request, status, response, ms}
document.addEventListener('convalido:verified', function (e) {
  document.querySelector('#conferma-ordine').disabled = false; // sblocca l'ordine solo dopo la verifica lato server!
});

4.6 Montaggio manuale e più widget

Per pagine generate dinamicamente: window.Convalido.mount(elemento, { apiKey, contractId, clauseSelector, clauseId, accountId }). Più widget nella stessa pagina sono ammessi e si passano il contatto già inserito; ognuno ha la propria transazione.

5. Verifica lato server (obbligatoria)

L'esito mostrato nel browser può essere manipolato da chi controlla la pagina. Prima di concludere l'ordine, il tuo backend deve confermare il token con la tua chiave API e controllare che contratto e clausola siano quelli attesi. Conserva l'evidence_id insieme all'ordine: è la chiave con cui recuperare il pacchetto probatorio.

PHP

$token = $_POST['convalido_evidence_token'] ?? '';
$ch = curl_init('https://api.convalido.it/v1/evidence/verify-token');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Api-Key: cv_live_LA_TUA_CHIAVE'],
    CURLOPT_POSTFIELDS => json_encode(['evidence_token' => $token]),
    CURLOPT_RETURNTRANSFER => true,
]);
$res = json_decode(curl_exec($ch), true);
$ok = ($res['valid'] ?? false) === true
   && $res['evidence']['contract_id'] === $idOrdine
   && $res['evidence']['clause_id'] === 'foro-competente';
if (!$ok) { /* clausola non approvata: non concludere l'ordine */ }
$evidenceId = $res['evidence']['evidence_id']; // da salvare con l'ordine

Node.js

const res = await fetch('https://api.convalido.it/v1/evidence/verify-token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.CONVALIDO_KEY },
  body: JSON.stringify({ evidence_token: req.body.convalido_evidence_token }),
});
const data = await res.json();
if (!data.valid || data.evidence.contract_id !== orderId) throw new Error('clausola non approvata');

cURL

curl -X POST https://api.convalido.it/v1/evidence/verify-token \
  -H "X-Api-Key: cv_live_LA_TUA_CHIAVE" -H "Content-Type: application/json" \
  -d '{"evidence_token":"eyJ…"}'

Per un gruppo di clausole verifica ogni token contenuto in convalido_evidence_token_all, oppure verifica il primo e controlla le altre con GET /v1/evidence/search?contract_id=…. Il token scade dopo 15 minuti: per controlli successivi usa GET /v1/evidence/{id}.

6. Integrazione nei CMS e negli e-commerce

Qualunque sia la piattaforma, l'integrazione è sempre la stessa coppia di operazioni: (a) inserire nella pagina del contratto il widget accanto al testo integrale delle clausole, cosa che richiede un punto in cui la piattaforma accetti HTML con uno <script>; (b) far verificare al server il token prima di concludere l'ordine, cosa che richiede un gancio (hook) di validazione del checkout o del modulo. Se la piattaforma non ha un backend programmabile, vedi 6.9. Plugin ufficiali per WordPress/WooCommerce e PrestaShop sono in preparazione; nel frattempo gli snippet che seguono sono sufficienti e restano validi.

6.1 WordPress (sito, moduli, pagine contrattuali)

Pagina: nell'editor a blocchi usa un blocco HTML personalizzato per il testo della clausola e il <div data-convalido …>; in Elementor/Divi/WPBakery l'equivalente è il widget "HTML". Lo <script src="…/widget.js" async> mettilo una volta sola, nel tema (wp_enqueue_script in functions.php) o con un plugin di "header/footer scripts". Gli editor WYSIWYG e i filtri di sicurezza rimuovono gli script dai contenuti degli utenti senza privilegi: usa un amministratore o il tema.

// functions.php del tema (o plugin): carica il widget su tutto il sito
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('convalido-widget', 'https://api.convalido.it/widget.js', [], null, ['strategy' => 'async']);
});

Modulo con Contact Form 7 / Gravity Forms / WPForms: inserisci il widget in un campo HTML del modulo; a verifica avvenuta il widget aggiunge il campo nascosto convalido_evidence_token al <form> che lo contiene, quindi lo ricevi con gli altri dati. Verifica il token nel gancio di validazione del plugin (es. wpcf7_validate, gform_validation) con la funzione seguente.

function convalido_verify(string $token, string $contractId, string $clauseId): ?array {
    $r = wp_remote_post('https://api.convalido.it/v1/evidence/verify-token', [
        'headers' => ['Content-Type' => 'application/json', 'X-Api-Key' => CONVALIDO_API_KEY],
        'body'    => wp_json_encode(['evidence_token' => $token]), 'timeout' => 10,
    ]);
    if (is_wp_error($r)) return null;
    $d = json_decode(wp_remote_retrieve_body($r), true);
    return (!empty($d['valid']) && $d['evidence']['contract_id'] === $contractId && $d['evidence']['clause_id'] === $clauseId) ? $d['evidence'] : null;
}

6.2 WooCommerce

Il widget va nella pagina di checkout, vicino al testo delle condizioni; il gancio più adatto è woocommerce_review_order_before_submit (subito prima del pulsante "Effettua ordine"). La validazione avviene in woocommerce_checkout_process, il salvataggio dell'evidence_id nell'ordine in woocommerce_checkout_create_order (compatibile con HPOS). Il contract_id non esiste ancora al momento del checkout: usa l'id della sessione/carrello e conserva l'evidence_id nell'ordine.

add_action('woocommerce_review_order_before_submit', function () {
    $contractId = 'cart-' . WC()->session->get_customer_id();
    echo '<div id="clausola-foro" class="cv-clausola">Art. 12 — Foro competente. …</div>';
    echo '<div data-convalido data-api-key="' . esc_attr(CONVALIDO_API_KEY) . '" data-clause-id="foro-competente" data-contract-id="' . esc_attr($contractId) . '" data-clause-selector="#clausola-foro" data-account-id="' . esc_attr(get_current_user_id()) . '"></div>';
});
add_action('woocommerce_checkout_process', function () {
    $token = sanitize_text_field($_POST['convalido_evidence_token'] ?? '');
    $ev = $token ? convalido_verify($token, 'cart-' . WC()->session->get_customer_id(), 'foro-competente') : null;
    if (!$ev) { wc_add_notice('Devi approvare specificamente la clausola con il codice ricevuto.', 'error'); }
    else { WC()->session->set('convalido_evidence_id', $ev['evidence_id']); }
});
add_action('woocommerce_checkout_create_order', function ($order) {
    $order->update_meta_data('_convalido_evidence_id', WC()->session->get('convalido_evidence_id'));
});

Con il checkout a blocchi (WooCommerce ≥ 8.3) i ganci PHP di rendering non valgono: inserisci il widget con un blocco HTML nella pagina di checkout o tramite l'API Additional Checkout Fields, e mantieni la validazione in woocommerce_store_api_checkout_order_processed / woocommerce_checkout_process.

6.3 Joomla 4 / 5

Pagina: crea un modulo Custom (HTML personalizzato) con il testo della clausola e il div del widget, e assegnalo alla posizione della pagina contrattuale; per inserire lo script, in Configurazione globale → Filtri di testo imposta "Nessun filtro" per il gruppo Super Users oppure usa un plugin come Sourcerer, oppure aggiungi lo <script> nel template (index.php o override). Modulo: con RSForm! Pro o Chronoforms inserisci il widget in un campo "HTML libero" e verifica il token nello script PHP di invio del modulo (RSForm: Script chiamato all'invio). Per VirtueMart o HikaShop: override del template del checkout per il widget e un plugin di sistema/pagamento che, nell'evento di conferma ordine, chiama /v1/evidence/verify-token con Joomla\CMS\Http\HttpFactory.

use Joomla\CMS\Http\HttpFactory;
$http = HttpFactory::getHttp();
$res  = $http->post('https://api.convalido.it/v1/evidence/verify-token', json_encode(['evidence_token' => $token]),
        ['Content-Type' => 'application/json', 'X-Api-Key' => $apiKey], 10);
$data = json_decode($res->body, true);
if (empty($data['valid']) || $data['evidence']['contract_id'] !== $contractId) { /* blocca */ }

6.4 PrestaShop 8 / 9

Un piccolo modulo: nel hook displayPaymentTop (in cima all'ultimo passo del checkout) stampa la clausola e il widget; nel hook actionValidateOrder verifica il token ricevuto dal form e salva l'evidence_id come nota/riferimento dell'ordine. In alternativa, senza modulo: override del template checkout/_partials/steps/payment.tpl per il widget, e il controllo del token in un modulo di pagamento personalizzato.

public function hookDisplayPaymentTop($params) {
    $this->context->smarty->assign(['cvKey' => Configuration::get('CONVALIDO_KEY'), 'cvContract' => 'cart-' . $this->context->cart->id]);
    return $this->display(__FILE__, 'views/templates/hook/widget.tpl');
}
public function hookActionValidateOrder($params) {
    $token = Tools::getValue('convalido_evidence_token');
    // → POST /v1/evidence/verify-token con X-Api-Key; se non valido: throw new PrestaShopException(...)
}

6.5 Magento / Adobe Commerce 2

Il checkout è un'app Knockout: inserisci il widget in un blocco personalizzato nella regione before-place-order (la stessa dei "Checkout Agreements", via checkout_index_index.xml e un template .html), passando il token nel payload di placeOrder (extension_attributes). Lato server, un plugin o observer su sales_order_place_before chiama /v1/evidence/verify-token e lancia LocalizedException se non valido; salva l'evidence_id come attributo dell'ordine.

6.6 Shopify

checkout.liquid non è più modificabile: sui piani standard il checkout non accetta script esterni. Due strade: (1) su Shopify Plus, un'estensione Checkout UI Extensions che incorpora il widget in un blocco personalizzato e una Shopify Function di validazione che blocca il pagamento senza token verificato (la verifica del token avviene nella tua app, tramite l'API); (2) su tutti i piani, spostare l'approvazione prima del checkout, in una pagina del tema (es. la pagina carrello o una pagina "Condizioni di fornitura" obbligatoria per i clienti B2B) con il widget inserito tramite Custom Liquid, e far verificare il token da un'app o da un servizio esterno (webhook orders/create → verifica → eventuale annullamento).

6.7 Wix, Webflow, Squarespace, HubSpot CMS

  • Wix: l'elemento "HTML incorporato" è un iframe su un dominio Wix, quindi il widget vedrebbe un dominio diverso dal tuo e le chiamate verrebbero rifiutate. Usa invece Velo con un Custom Element (che vive nel DOM della tua pagina) per caricare widget.js, e un modulo backend Velo (.jsw) che verifica il token con fetch prima di registrare l'ordine.
  • Webflow: incorpora clausola e widget con un elemento Embed e lo script in Custom code del sito. Webflow non ha un backend: la verifica va fatta in un Webflow Logic/Cloud function, in una funzione serverless (Cloudflare Workers, Netlify, Vercel) chiamata dal modulo, o dal tuo CRM alla ricezione dell'invio (vedi 6.9).
  • Squarespace: Code Block (piani Business o superiori) per clausola e widget; per la verifica valgono le stesse opzioni di Webflow.
  • HubSpot CMS: modulo personalizzato con HTML libero; la verifica del token in una serverless function di HubSpot o in un workflow che, alla ricezione del modulo, chiama l'API.

6.8 Applicazioni su misura (Laravel, Symfony, Django, Rails, Node, .NET…)

Il widget è HTML puro; la verifica è una chiamata HTTPS con la chiave in header: gli esempi della sezione 5 valgono per qualsiasi linguaggio. Consiglio: crea un unico endpoint interno "conferma ordine" che verifica il token, salva evidence_id e solo poi prosegue.

6.9 Se non hai un backend

La verifica del token serve a impedire che chi controlla il browser simuli un'approvazione. Senza server puoi comunque lavorare in sicurezza: il widget consegna evidence_id insieme ai dati del modulo (o del CRM); prima di erogare il servizio, controlla che quell'evidenza esista ed appartenga a quel contratto nella dashboard (ricerca per contratto o per id cliente) o con una chiamata a GET /v1/evidence/{id} da un'automazione (Make, Zapier, n8n) che poi aggiorna il tuo gestionale. Un webhook server-to-server alla verifica è in programma e semplificherà questo scenario.

6.10 Regole che valgono ovunque

  • Il selettore del widget deve puntare all'elemento con il testo integrale della clausola: quello è ciò che viene inviato, confrontato e conservato.
  • Il dominio della pagina deve essere tra i domini autorizzati della chiave (anche il dominio di staging, se lo usi).
  • Lo script va caricato dalla pagina stessa, non dentro un iframe di un altro dominio.
  • Usa una chiave cv_test_ finché l'integrazione non è completa; poi sostituiscila con la chiave live.

7. Riferimento API

Autenticazione. Gli endpoint server-to-server richiedono l'header X-Api-Key: cv_live_…. Gli endpoint del widget ricevono la chiave nel corpo e verificano il dominio di provenienza (Origin). Le risposte di errore hanno sempre la forma {"status": "codice", "message": "testo"}.

POST /v1/otp/request — widget

Avvia l'approvazione di una o più clausole e invia il codice.

{
  "merchant_api_key": "cv_live_…",
  "domain": "shop.tuaazienda.it",
  "contract_id": "ORD-2026-000123",
  "contact": "mario.rossi@azienda.it",     // oppure "+39 3xx xxxxxxx"
  "channel": "email",                        // "email" | "sms"
  "merchant_account_id": "CLIENTE-42",       // facoltativo
  "contract_text": "CONDIZIONI GENERALI …",  // facoltativo: testo integrale del contratto (max 400.000 caratteri)
  "contract_version": "CGF-2026-08",         // facoltativo
  "widget_version": "0.3",                   // impostato dal widget; le integrazioni API possono ometterlo
  "clause_id": "foro-competente",            // singola …
  "clause_text": "Art. 12 — …"
  // … oppure gruppo (max 20):
  // "clauses": [{"clause_id":"foro-competente","clause_text":"…"}, {"clause_id":"rinnovo-tacito","clause_text":"…"}]
}

Risposta 201:

{
  "transaction_id": "75c1343e-…",
  "group_id": null,                           // valorizzato nel gruppo
  "clauses": [{"clause_id":"foro-competente","transaction_id":"75c1343e-…"}],
  "expires_at": "2026-08-29T10:05:00.000Z",
  "channel": "email",
  "contact": "mario.rossi@azienda.it",
  "contract_version": "CGF-2026-08",
  "contract_hash": "0f4c…",                   // impronta del contract_text ricevuto (null se non inviato)
  "test": false                               // true con una chiave cv_test_
}

POST /v1/otp/verify — widget

{ "transaction_id": "75c1343e-…", "otp": "123456", "clause_text": "Art. 12 — …", "contract_text": "CONDIZIONI GENERALI …" }
// gruppo: "clauses": [{"clause_id":"…","clause_text":"…"}, …] con i testi mostrati in quell'istante
// contract_text va ripetuto se era stato inviato alla richiesta (altrimenti 400 missing_contract_text)

Risposta 200:

{
  "status": "verified",
  "evidence_token": "eyJ…", "evidence_id": "c99c1fc5-…", "transaction_id": "75c1343e-…",
  "clause_id": "foro-competente", "clause_hash": "b5e0a6bf…", "contract_hash": "0f4c…", "verified_at": "2026-08-29T10:01:12.607Z",
  "group_id": null,
  "evidences": [ { "evidence_id": "c99c1fc5-…", "evidence_token": "eyJ…", "clause_id": "foro-competente",
                   "clause_hash": "b5e0a6bf…", "timestamp": { "status": "ok", "provider": "sectigo", "tsa_time": "2026-08-29T10:01:13.000Z" } } ]
}

POST /v1/evidence/verify-token — tuo server

Corpo: {"evidence_token": "eyJ…"}. Risposta: {"valid": true, "evidence": {…}} con lo stesso oggetto di search. Errori: invalid_token_signature, token_expired (410), token_merchant_mismatch (403).

GET /v1/evidence/{evidence_id} — tuo server

{
  "evidence_id": "c99c1fc5-…", "transaction_id": "75c1343e-…", "status": "verified",
  "contract_id": "ORD-2026-000123", "contract_version": "CGF-2026-08", "contract_hash": "0f4c…",
  "clause_id": "foro-competente", "clause_hash": "b5e0a6bf…", "widget_version": "0.3",
  "contact_channel": "email", "contact": "mario.rossi@azienda.it", "domain": "shop.tuaazienda.it",
  "requested_at": "2026-08-29T10:00:09.451Z", "verified_at": "2026-08-29T10:01:12.607Z",
  "delivery_provider": "brevo-email", "evidence_hash": "48ecd1b2…", "chain_seq": 812,
  "group_id": null, "group_size": 1, "environment": "live",
  "timestamp": { "provider": "sectigo", "tsa_time": "2026-08-29T10:01:13.000Z" },
  "clause_text": "Art. 12 — …", "clause_hash_request": "b5e0a6bf…",
  "contract_text": "CONDIZIONI GENERALI …", "contract_hash_request": "0f4c…",
  "ip_request": "151.11.48.1", "ip_verify": "151.11.48.1", "user_agent": "Mozilla/5.0 …", "attempts": 1,
  "chain_hash": "8623848f…", "chain_prev_hash": "e4a88a64…"
}

GET /v1/evidence/{evidence_id}/bundle

Il file JSON com'è conservato, con header X-Evidence-Sha256.

GET /v1/evidence/search

Parametri: email oppure phone (valore completo: la ricerca confronta l'impronta), account (il tuo id cliente), clause_id, contract_id, status (default verified; all per tutti), from/to (YYYY-MM-DD), limit (max 100), offset. Risposta: {"total", "limit", "offset", "items": [...]}.

GET /v1/health

{"status":"ok","time":"…"} — per il monitoraggio.

8. Codici di errore

HTTPstatusQuandoCosa fa il widget
400invalid_contact, missing_clause_text, missing_clause_id, invalid_channelDati mancanti o non validi.Mostra il messaggio.
400invalid_otpCodice errato; attempts_remaining nella risposta.Mostra i tentativi rimasti.
401invalid_api_keyChiave sconosciuta o revocata.Errore.
402subscription_required, payment_requiredProva esaurita senza piano attivo, o pagamento non riuscito (solo chiavi live).Errore: attiva il piano dalla dashboard.
403domain_not_allowed, origin_mismatch, merchant_suspendedDominio non autorizzato, Origin diverso dal dominio dichiarato, account sospeso.Errore.
409clause_mismatchIl testo mostrato alla verifica è cambiato rispetto alla richiesta; nel gruppo clauses[] elenca quali.Riparte dal primo passo.
409contract_mismatchIl testo integrale del contratto (se trasmesso) è cambiato tra richiesta e verifica.Riparte dal primo passo.
400missing_contract_text, contract_text_too_longcontract_text inviato alla richiesta ma non alla verifica, oppure oltre 400.000 caratteri.Mostra il messaggio.
409too_many_attempts, already_usedTentativi esauriti o transazione già verificata.Riparte dal primo passo.
410expiredCodice scaduto (5 minuti).Riparte dal primo passo.
429rate_limitedTroppe richieste per IP, contatto o account; retry_after in secondi.Mostra il messaggio.
502delivery_failedNessun fornitore ha consegnato il codice.Per gli SMS propone l'email.

9. Pacchetto probatorio

Per ogni clausola verificata Convalido genera evidence.json (schema convalido.evidence.v2): un file scritto in sola lettura e nominato con la propria impronta SHA-256, quindi non modificabile senza che chiunque lo verifichi se ne accorga. È costruito per rispondere, da solo, a ciò che il predisponente deve provare secondo Cass. 20945/2026: chi ha approvato, che cosa, in quale versione, quando e con quale riferibilità alle clausole gravose. Contiene:

  • la clausola approvata (clause_id, clause_text integrale, clause_hash alla verifica e clause_hash_request alla richiesta);
  • tutte le clausole vessatorie approvate con lo stesso codice, ciascuna con testo integrale e impronta (clauses[]; un solo elemento nell'approvazione singola) e, nel gruppo, l'oggetto group con posizione e impronte delle altre evidenze;
  • il testo integrale del contratto in contract (contract_id, contract_version, text, hash alla verifica, hash_request alla richiesta) — presente quando l'integrazione passa data-contract-selector / contract_text;
  • il firmatario: contact in chiaro e contact_hash, ip_request, ip_verify, user_agent, widget_version (versione dell'interfaccia mostrata; null nelle integrazioni API);
  • gli istanti requested_at e verified_at (UTC, millisecondi);
  • la prova dell'invio del codice in otp_delivery: fornitore, canale, message_id del fornitore (Brevo/SMTP), tentativo e istante di invio;
  • domain, environment (live/test), outcome e il riferimento alla marca temporale.

I pacchetti generati prima del 29/08/2026 hanno schema v1 (senza clauses[], contract, widget_version, otp_delivery): restano validi e verificabili, sono immutabili per definizione.

Subito dopo la creazione, l'impronta del file viene inviata a una Time Stamping Authority RFC 3161 (Sectigo, con DigiCert e una terza autorità come riserva automatica) e il token .tsr restituito è conservato accanto al file. Le evidenze sono inoltre concatenate in una catena di hash: ogni anello contiene l'impronta del precedente, così una modifica o una cancellazione rompe la catena.

Dalla dashboard si scarica, per ogni approvazione, uno ZIP con evidence.json, chain.json (posizione e hash della catena), timestamp-<tsa>.tsr e un README.txt con le istruzioni.

Verifica

  • Automatica: carica lo ZIP (o il solo evidence.json) su https://www.convalido.it/verifica: integrità, corrispondenza con l'archivio, catena e marca temporale vengono controllate e spiegate punto per punto. Chiunque può farlo, senza account.
  • Manuale: sha256sum evidence.json deve coincidere con evidence_hash dell'API e con l'impronta marcata nel .tsr; openssl ts -verify -data evidence.json -in timestamp-sectigo.tsr -CAfile <catena della TSA> verifica firma e ora della marca.

10. Piattaforma e chiavi di test

Ci sono due modi per provare senza conseguenze, pensati per momenti diversi.

10.1 La piattaforma di test — test.convalido.it

È una pagina contrattuale d'esempio, pubblica, che usa il widget vero collegato a un account dimostrativo: serve a capire il flusso prima di registrarsi o di scrivere codice. Sotto una clausola fittizia inserisci la tua email o il tuo cellulare, ricevi davvero il codice e lo confermi; mostra due clausole approvate in gruppo e una singola. Il pannello "Cosa succede" a fianco mostra in tempo reale:

  • ogni chiamata del widget all'API con richiesta e risposta (chiave troncata, codice mai mostrato);
  • ciò che viene registrato: il record della transazione, le consegne del codice, la posizione nella catena di hash, la marca temporale e il bundle di evidenza così com'è scritto su disco, scaricabile;
  • come il merchant vedrebbe la stessa evidenza tramite l'API;
  • la telemetria degli errori (prova un codice sbagliato, o il pulsante che modifica il testo della clausola per provocare clause_mismatch).

Le approvazioni fatte lì appartengono all'account dimostrativo, sono cancellate automaticamente dopo pochi giorni e non hanno alcun valore probatorio.

10.2 Le chiavi di test — cv_test_…

Quando hai un account e vuoi provare l'integrazione sul tuo sito, genera dalla dashboard una chiave di test. Funziona esattamente come una chiave live (stessi endpoint, stessi errori, codici inviati davvero, evidenze e marche temporali generate) ma:

  • le approvazioni non si pagano e non consumano le approvazioni di prova né quelle incluse nel piano;
  • nella dashboard e nell'API sono marcate test (badge e campo environment) e il bundle riporta "environment": "test";
  • funziona anche senza piano attivo.

Per andare in produzione basta sostituire la chiave con una cv_live_…: nessun'altra modifica al codice.

11. Dashboard (account.convalido.it)

  • Approvazioni — ricerca per contatto, contratto, clausola, id cliente, stato, date; dettaglio con testo approvato, firmatario, IP, orari, consegna, evidenza, marca temporale; download del pacchetto probatorio.
  • Consumi — conteggio operazione per operazione del mese (prova / incluse / extra, supplementi SMS) e andamento mensile.
  • Utenti — inviti con ruolo owner (gestisce tutto) o viewer (consulta e scarica).
  • Impostazioni — chiavi API live e test, domini autorizzati, password, verifica in due passaggi (codice via email o app di autenticazione).
  • Abbonamento — piano, attivazione e pagamento, storico.

Ogni ricerca, consultazione e download viene registrato nel log accessi: rafforza la credibilità della prova in caso di contestazione.

12. Sicurezza e limiti

  • Codici a 6 cifre generati con CSPRNG, conservati solo come hash; 5 tentativi per transazione; validità 5 minuti; limiti per IP, contatto e account.
  • Chiavi API conservate solo come hash e vincolate ai domini autorizzati; revocabili in qualsiasi momento.
  • Contatto del firmatario cifrato in tabella separata e in chiaro nel pacchetto probatorio (deve identificare chi ha firmato); nelle tabelle di ricerca solo l'impronta.
  • Il confronto del testo tra richiesta e verifica avviene sull'impronta dopo normalizzazione degli spazi: un ritorno a capo diverso non genera un falso clause_mismatch, una parola cambiata sì.
  • Limiti: clause_text fino a 20.000 caratteri; 20 clausole per gruppo; 5 chiavi attive; ricerca API max 100 risultati per pagina.
  • Cosa non è: una firma elettronica avanzata o qualificata (per i contratti che richiedono forma scritta a pena di nullità serve altro); uno strumento per contratti con consumatori.

13. Buone pratiche legali per l'integrazione

  1. Mappa le clausole vessatorie con il tuo legale (foro, arbitrato, limitazioni di responsabilità, decadenze, rinnovi taciti, recesso unilaterale) e decidi se approvarle singolarmente o in gruppo: in entrambi i casi ogni clausola va identificata e riportata per esteso, mai riassunta.
  2. Mostra il testo integrale accanto al widget, non un titolo o un link: è quel testo che viene firmato e conservato.
  3. Non concludere l'ordine senza aver verificato il token lato server e salvato l'evidence_id.
  4. Usa un identificativo cliente coerente con il tuo gestionale: in caso di contestazione cercherai per cliente.
  5. Re-papering: per i contratti già in essere conclusi con la sola spunta, valuta una nuova approvazione tramite il widget.
  6. Ricorda che il servizio è per rapporti tra imprese: nei contratti con consumatori la disciplina è diversa (artt. 33-36 Codice del Consumo).

14. Domande frequenti tecniche

Posso usare il widget in una single-page application?
Sì: carica widget.js una volta e chiama window.Convalido.mount() quando il contratto viene renderizzato.
Il widget funziona dentro un iframe di terzi?
Le chiamate devono partire da una pagina il cui dominio è nella tua allowlist: se l'iframe è servito da un tuo dominio autorizzato, sì.
Cosa succede se il firmatario chiude la pagina dopo aver ricevuto il codice?
La transazione scade dopo 5 minuti e non viene contata; al ritorno ripeterà la richiesta.
Posso ricevere una notifica quando una clausola viene approvata?
L'evento convalido:verified nel browser e la verifica del token nel backend sono il punto di aggancio; un webhook server-to-server è in programma.
Quanto tempo restano disponibili le evidenze?
Dieci anni dalla verifica, scaricabili in qualsiasi momento dalla dashboard o via API.

Prova sulla piattaforma di test Crea un account Supporto tecnico