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:
- Lato pagina — il widget JavaScript, che sostituisce il checkbox sotto le clausole e gestisce invio e verifica del codice.
- 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)
- Crea l'account su account → Registrazione: ottieni la dashboard e le approvazioni gratuite di prova. Conferma l'email.
- 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. - 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> - Prova il flusso: inserisci la tua email, ricevi il codice, confermalo. L'approvazione compare nella dashboard con il badge test.
- Nel backend, prima di concludere l'ordine, verifica il token che il widget ha aggiunto al form.
- 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
| Clausola | Il 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. |
|---|---|
| Contratto | Il contract_id è il riferimento all'ordine o al contratto nel tuo sistema. Più clausole dello stesso contratto condividono lo stesso contract_id. |
| Transazione | Una 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 singola | Un widget sotto ogni clausola, un codice per ciascuna. I widget della stessa pagina condividono il contatto già inserito. |
| Approvazione in gruppo | Un 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. |
| Evidenza | Il file immutabile prodotto per ogni clausola verificata (sezione 9), identificato da un evidence_id. |
| Token di esito | Un 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 API | cv_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
| Attributo | Obbligatorio | Significato |
|---|---|---|
data-api-key | sì | Chiave API. È visibile nel sorgente: per questo le chiamate sono accettate solo dai domini autorizzati (header Origin). |
data-contract-id | sì | Riferimento all'ordine/contratto (max 128 caratteri). |
data-clause-selector | sì* | Selettore CSS dell'elemento (o degli elementi) con il testo. In alternativa data-clause-text con il testo esplicito (solo modo singolo). |
data-clause-id | singola | Identificativo stabile della clausola. Nel gruppo l'id di ogni clausola è il valore di data-convalido-clause. |
data-contract-selector | no (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-version | no | Etichetta di versione delle condizioni (max 64 caratteri, es. CGF-2026-08), registrata accanto a contract_id nel pacchetto e nell'API. |
data-account-id | no | Il tuo identificativo del cliente: permette di cercare in dashboard tutte le approvazioni di un cliente. |
data-input-name | no | Nome base dei campi nascosti aggiunti al form (default convalido_evidence_token). |
data-debug | no | Emette 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 conclause_id,evidence_id,evidence_tokendi ogni clausola (utile nel gruppo).
4.5 Eventi
Tutti emessi sull'elemento host, con bubbling, quindi intercettabili anche su document:
convalido:sent | codice inviato; detail = risposta di /v1/otp/request |
convalido:verified | approvazione riuscita; detail = risposta di /v1/otp/verify (con evidences[]) |
convalido:error | detail = {code, message, payload} |
convalido:request | solo 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 confetchprima 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
| HTTP | status | Quando | Cosa fa il widget |
|---|---|---|---|
| 400 | invalid_contact, missing_clause_text, missing_clause_id, invalid_channel | Dati mancanti o non validi. | Mostra il messaggio. |
| 400 | invalid_otp | Codice errato; attempts_remaining nella risposta. | Mostra i tentativi rimasti. |
| 401 | invalid_api_key | Chiave sconosciuta o revocata. | Errore. |
| 402 | subscription_required, payment_required | Prova esaurita senza piano attivo, o pagamento non riuscito (solo chiavi live). | Errore: attiva il piano dalla dashboard. |
| 403 | domain_not_allowed, origin_mismatch, merchant_suspended | Dominio non autorizzato, Origin diverso dal dominio dichiarato, account sospeso. | Errore. |
| 409 | clause_mismatch | Il testo mostrato alla verifica è cambiato rispetto alla richiesta; nel gruppo clauses[] elenca quali. | Riparte dal primo passo. |
| 409 | contract_mismatch | Il testo integrale del contratto (se trasmesso) è cambiato tra richiesta e verifica. | Riparte dal primo passo. |
| 400 | missing_contract_text, contract_text_too_long | contract_text inviato alla richiesta ma non alla verifica, oppure oltre 400.000 caratteri. | Mostra il messaggio. |
| 409 | too_many_attempts, already_used | Tentativi esauriti o transazione già verificata. | Riparte dal primo passo. |
| 410 | expired | Codice scaduto (5 minuti). | Riparte dal primo passo. |
| 429 | rate_limited | Troppe richieste per IP, contatto o account; retry_after in secondi. | Mostra il messaggio. |
| 502 | delivery_failed | Nessun 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_textintegrale,clause_hashalla verifica eclause_hash_requestalla 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'oggettogroupcon posizione e impronte delle altre evidenze; - il testo integrale del contratto in
contract(contract_id,contract_version,text,hashalla verifica,hash_requestalla richiesta) — presente quando l'integrazione passadata-contract-selector/contract_text; - il firmatario:
contactin chiaro econtact_hash,ip_request,ip_verify,user_agent,widget_version(versione dell'interfaccia mostrata;nullnelle integrazioni API); - gli istanti
requested_ateverified_at(UTC, millisecondi); - la prova dell'invio del codice in
otp_delivery: fornitore, canale,message_iddel fornitore (Brevo/SMTP), tentativo e istante di invio; domain,environment(live/test),outcomee 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.jsondeve coincidere conevidence_hashdell'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 campoenvironment) 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_textfino 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
- 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.
- Mostra il testo integrale accanto al widget, non un titolo o un link: è quel testo che viene firmato e conservato.
- Non concludere l'ordine senza aver verificato il token lato server e salvato l'
evidence_id. - Usa un identificativo cliente coerente con il tuo gestionale: in caso di contestazione cercherai per cliente.
- Re-papering: per i contratti già in essere conclusi con la sola spunta, valuta una nuova approvazione tramite il widget.
- 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.jsuna volta e chiamawindow.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:verifiednel 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