Webhook in Uscita: Avvisare un Sistema Esterno alla Firma
Quando un cliente firma, qualcosa di solito deve partire anche fuori da SalesControl: creare il suo spazio sulla tua piattaforma, aprire una scheda nel gestionale, avvisare un’automazione. Un webhook in uscita fa esattamente questo: alla firma (o a un passo di un flusso automatico) SalesControl chiama un indirizzo che indichi tu e gli manda i dati del contratto più i campi che scegli.
I webhook in uscita sono disponibili dal piano Professional in su. Li gestiscono gli admin e i membri col permesso “Può modificare i modelli di contratto”.
A cosa serve
L’esempio vero da cui è nata la funzione: un’agenzia crea a ogni cliente un sottodominio sul proprio sito. Prima lo faceva a mano, dopo ogni firma. Ora ha un webhook che chiama il proprio server e gli manda un campo subdomain composto da {{customer_name}}.{{customer_last_name}}: il sottodominio nasce da solo.
Attenzione a non confonderlo col suo opposto: i webhook “in entrata” (lead dal gestionale, firma dal gestionale, eventi) sono qualcosa che arriva a SalesControl. Questo è il verso opposto: qualcosa che parte da SalesControl.
Dove si configura
Sul modello di contratto, non nelle impostazioni generali: così ogni modello può avvisare sistemi diversi. In Contratti › Modelli, apri un modello e scorri fino alla sezione Webhook (firma e flussi).
Su un modello appena creato e mai salvato la sezione dice “Salva il modello per aggiungere un webhook”. Salva una volta e il bottone + Webhook si accende.
C’è anche una vista d’insieme: Impostazioni › Integrazioni › Webhook › Webhook in uscita elenca tutti quelli dell’agenzia con nome, modello, evento, indirizzo, stato e ultimo esito, e il bottone Apri il modello per andarli a modificare. Da lì si legge anche il registro delle consegne.
Alla firma o in un passo di flusso
Nella modale, Quando parte sceglie l’evento:
- Dopo la firma (
contract.signed): parte da solo quando il cliente firma un contratto di questo modello. È il caso descritto nel resto della guida. - Passo di un flusso (
contract.flow_step): parte solo quando un flusso automatico del modello arriva a un passo Webhook che lo sceglie, anche prima della firma (per esempio dopo un sollecito rimasto senza risposta).
Il messaggio di un passo di flusso è lo stesso della firma (qui sotto), con event = contract.flow_step e in più il blocco flow:
"flow": { "node_id": "n_3fa81c02", "phase": "main" }
node_idè il passo del flusso;phasela sua colonna:main(Contratto inviato),signed(Firmato) oexpired(Link scaduto).pdf_urlc’è solo se il contratto è già firmato, altrimenti ènull. Prima della firma, per un contratto mandato a un lead, anchecliente.idecliente.contract_idsononull.- Se il tuo server non risponde (errore di rete, 429 o 5xx), il passo riprova da solo più tardi, fino a quattro volte nell’arco di qualche ora; un webhook spento fa saltare il passo.
Indirizzo, header e sicurezza
Nella modale del webhook compili:
- Nome — serve solo a te, per ritrovarlo nell’elenco e nel registro.
- Indirizzo (URL) — dove mandare il messaggio. Solo
https://: un indirizzo in chiaro non è accettato. Sono rifiutati anche gli indirizzi di rete interna, con il messaggio “Questo indirizzo è su una rete privata e non si può chiamare”. - Header — facoltativi, fino a 10. Servono se il sistema che riceve chiede una chiave (per esempio
Authorization). - Attivo / Spento — un webhook spento resta configurato ma non viene chiamato.
I valori degli header restano cifrati e non si rivedono mai più. Riaprendo il webhook il campo mostra “•••• salvato · lascia vuoto per conservare”: lascialo com’è e il valore salvato resta, scrivici sopra per sostituirlo. Se cambi nome a un header, il vecchio valore non si ritrova più e va riscritto.
Due controlli lavorano in silenzio a ogni consegna, non solo quando salvi: l’indirizzo viene ricontrollato al momento dell’invio (una regola nuova vale anche per un webhook salvato mesi fa) e i reindirizzamenti non si seguono. Se il tuo server risponde con un redirect, la consegna risulta fallita: metti nel campo l’indirizzo finale.
Alcuni header li mettiamo noi e non si possono sovrascrivere: content-type, user-agent e tutti quelli che iniziano per x-salescontrol-.
I campi personalizzati e le variabili
Nella sezione Campi aggiungi le coppie Nome del campo e Valore che il tuo sistema si aspetta — fino a 30, con valori fino a 2.000 caratteri. Nel valore usi le variabili dai chip sotto il campo, da sole o mescolate al testo ({{customer_name}}.{{customer_last_name}}, cliente-{{customer_id}}…).
| Gruppo | Variabili |
|---|---|
| Contatto | customer_name (il primo nome), customer_full_name (il nome completo), customer_last_name, customer_email, customer_phone |
| Contratto | service_name, contract_value, duration_months, payment_type, currency, start_date, signature_date |
| Agenzia e team | agency_name, closer_name, setter_name |
| Riferimenti | customer_id, contract_id, chat_link, pdf_link |
Una variabile riconosciuta ma senza valore (un cliente senza telefono, un contratto senza setter) diventa stringa vuota: il tuo sistema non riceve mai la scritta {{customer_phone}} al posto di un numero. Una variabile scritta male, invece, resta com’è: così il refuso si vede nel registro delle consegne e lo correggi.
Un campo non può chiamarsi event, occurred_at, agency, contract, cliente, servizio, team o flow: sono i blocchi standard del messaggio e devono voler dire sempre la stessa cosa.
Com’è fatto il messaggio
La chiamata è un POST con un corpo JSON. Dentro ci sono il blocco standard e, al primo livello accanto a lui, i tuoi campi personalizzati: il tuo sistema legge subdomain e basta, senza doverlo pescare dentro un oggetto annidato.
{
"event": "contract.signed",
"occurred_at": "2026-09-18T10:32:07.512Z",
"agency": {
"id": "3f2a…",
"name": "MedWave"
},
"contract": {
"id": "8c41…",
"template_id": "b07d…",
"template_name": "Contratto servizi marketing",
"signed_at": "2026-09-18T10:31:58.004Z",
"valore_contratto": 4500,
"durata_mesi": 6,
"tipo_pagamento": "mensile",
"currency": "EUR",
"data_inizio": "2026-10-01",
"pdf_url": "https://…"
},
"cliente": {
"id": "d914…",
"contract_id": "d914…",
"nome": "Mario Rossi",
"primo_nome": "Mario",
"cognome": "Rossi",
"email": "mario.rossi@esempio.it",
"telefono": "+39 333 1234567",
"lingua": "it",
"chat_url": "https://portal.salescontrol.app/c/…"
},
"servizio": {
"id": "5ab2…",
"nome": "Gestione campagne"
},
"team": {
"closer": { "nome": "Luca", "email": "luca@esempio.it" },
"setter": { "nome": "Jenny", "email": "jenny@esempio.it" }
},
"subdomain": "mario.rossi"
}
L’ultima riga, subdomain, è il campo personalizzato dell’esempio: al posto suo ci sono i tuoi. Un valore che non esiste arriva come null (per esempio telefono per un cliente che non è mai stato un lead, o chat_url se il piano non include la chat). Il pdf_url è un link temporaneo al PDF firmato: vale sette giorni, quindi scarica il file se ti serve conservarlo.
Se il cliente firma un secondo contratto in parallelo (un altro servizio mentre il primo è ancora in corso), il messaggio parla sempre della stessa persona: cliente.id (e la variabile customer_id) è l’identificativo che hai ricevuto alla sua prima firma, e nome, email, telefono, lingua e chat_url sono i suoi. cliente.contract_id invece è il contratto appena firmato: servizio, closer, setter e importi del messaggio sono i suoi. Per un cliente con un solo contratto i due identificativi coincidono.
Insieme al corpo arrivano tre header nostri: x-salescontrol-event (qui contract.signed), x-salescontrol-delivery (l’identificativo di quella consegna) e la firma, qui sotto. Per non agire due volte sullo stesso contratto, il riferimento affidabile è contract.id: è sempre lo stesso anche su un secondo tentativo o su un Riprova.
La firma x-salescontrol-signature
Ogni webhook ha il suo Segreto della firma, che trovi nella modale con i bottoni Copia e Rigenera. Serve a chi riceve per verificare che il messaggio arrivi davvero da voi e non sia stato toccato per strada.
L’header ha questa forma:
x-salescontrol-signature: t=1758190327,v1=4f0c1d…
tè il momento dell’invio, in secondi (il formato “unix”).v1è il codice di verifica, in esadecimale.
Per controllarla, chi riceve fa tre cose:
Il testo grezzo della richiesta, prima di trasformarlo in oggetto: rileggerlo e riscriverlo cambierebbe spazi e ordine delle chiavi, e la verifica non tornerebbe mai.
È il valore di t, un punto, e il corpo: 1758190327.{"event":"contract.signed",…}.
Il risultato in esadecimale deve essere identico a v1. È la stessa ricetta che usa Stripe per i suoi webhook: qualsiasi libreria che già li verifica va bene.
Conviene anche rifiutare i messaggi troppo vecchi, confrontando t con l’ora attuale. Il timestamp è fresco a ogni tentativo: il terzo, che parte sei secondi dopo, non nasce già scaduto.
Premendo Rigenera: “Il segreto attuale smette di valere: chi verifica la firma dovrà usare quello nuovo”. Aggiorna prima il tuo sistema, o le consegne successive verranno rifiutate.
Il registro delle consegne e «Riprova»
Ogni tentativo lascia una riga. Prima di mandare un contratto vero puoi premere Invia una prova nella modale: parte una consegna reale con dati di esempio, e il registro la segna col badge Prova.
Il registro completo — Ultime consegne, 50 righe — sta in Impostazioni › Integrazioni › Webhook › Webhook in uscita, con Data, Webhook, Esito, HTTP, Tentativi, e per ogni riga Dettagli (che mostra Messaggio inviato e Risposta) e Riprova, che rimanda lo stesso identico corpo.
Su come si ritenta da sé:
- Tre tentativi, subito, dopo 2 secondi e dopo 6 secondi.
- Si ritenta solo quando ha senso: errore di rete o risposta 5xx del tuo server. Una risposta 4xx (per esempio un 401 per una chiave sbagliata) è una risposta, e ripeterla non cambierebbe nulla: la consegna risulta fallita subito.
- Timeout a 10 secondi per tentativo.
- Finiti i tentativi non c’è nessuna ripetizione automatica più tardi: si riprova a mano dal registro.
Il registro conserva il corpo inviato, mai gli header: i loro valori restano cifrati e non si rileggono da nessuna parte.
Quando NON parte
- Se gli effetti della firma non sono riusciti. Il webhook parte a effetti andati, quando il cliente esiste davvero e il messaggio può parlarne. Risolvendo con Riprova effetti parte anche lui.
- Per una firma arrivata dal gestionale (il webhook in entrata “contratto firmato”): quella firma non passa da un modello, e senza modello non c’è nessun webhook da chiamare.
- Se il webhook è spento, o se appartiene a un modello diverso da quello del contratto firmato.
- Se hai eliminato il modello: spariscono anche i suoi webhook e il loro registro.
La chiamata parte in secondo piano: anche se il tuo server ci mette dieci secondi e serve un secondo tentativo, il cliente vede subito la sua pagina e riceve il PDF.
Un dettaglio utile: customer_phone viene dal lead d’origine, perché la scheda cliente non ha un campo telefono. Un contratto mandato a un cliente che non è mai stato un lead lo manda vuoto.