8. Integrazioni — Webhook in uscita

I webhook in uscita permettono a NoviaMind di notificare i tuoi sistemi nel momento in cui i dati CRM cambiano — senza dover interrogare l'API in continuazione. Usi tipici: avviare un workflow n8n / Make / Zapier quando viene acquisito un nuovo prospect, o mantenere sincronizzato uno strumento interno.

Accedi da Integrazioni → Webhook in uscita nella sezione CRM.


8.9 Eventi disponibili

EventoInviato quando
lead.createdViene creato un prospect
lead.updatedViene aggiornato un prospect
property.createdViene creato un immobile
property.updatedViene aggiornato un immobile
realtor.createdViene creato un agente
realtor.updatedViene aggiornato un agente

Un webhook si iscrive a uno o più eventi — solo quelli attivano una consegna verso il tuo URL.


8.10 Creare un webhook

  1. Vai su Integrazioni → Webhook in uscita
  2. Clicca su «Aggiungi webhook»
  3. Inserisci il tuo URL di callback (deve essere https:// in produzione; http:// è accettato solo per test locali, ad es. un tunnel ngrok)
  4. Aggiungi facoltativamente una descrizione (es. «workflow n8n produzione»)
  5. Seleziona gli eventi a cui iscriverti
  6. Clicca su «Salva»

⚠️ Salva subito il segreto di firma. Dopo la creazione, NoviaMind mostra il segreto (whsec_...) una sola volta. Ogni visualizzazione successiva lo mostra mascherato. Se lo perdi, elimina il webhook e creane uno nuovo.

💡 Gli URL che puntano a indirizzi di rete privati o interni vengono rifiutati per motivi di sicurezza.


8.11 Cosa riceve il tuo endpoint

Ogni consegna è una richiesta HTTP POST con un corpo JSON:

{
  "id": "d3f5c9a2-...",
  "event": "lead.created",
  "team_id": "b81e07c4-...",
  "created_at": "2026-07-30T14:20:11.000Z",
  "data": { "...": "l'entità creata o aggiornata — la forma dipende da `event`, vedi sotto" }
}

E queste intestazioni:

IntestazioneContenuto
X-Webhook-IdIdentificativo univoco della consegna (anche l'id nel corpo)
X-Webhook-EventNome dell'evento (es. lead.created)
X-Webhook-Signaturesha256=<hex> — firma HMAC-SHA256 del corpo grezzo

Formato di data per entità

data non è un DTO di risposta curato: è la riga completa del contatto/immobile/agente così com'è memorizzata, quindi può contenere più campi di quelli documentati qui sotto (tratta i campi sconosciuti come compatibili con le evoluzioni future, non rifiutarli) e ogni campo nullable che non usi vale semplicemente null.

lead.created / lead.updateddata è il contatto. Cattura reale (da una chiamata con l'agente IA, con segreti/dati personali accorciati e notes e i blocchi di raccolta dati dell'IA in custom_fields / raw_data troncati per brevità — in pratica notes contiene il riepilogo completo della chiamata e custom_fields.* una voce per ogni campo di raccolta configurato):

{
  "id": "d80029a4-8324-4fad-afe2-ad1af76319fe",
  "team_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "source_provider": "noviamind",
  "external_reference": "conv-XXX",
  "first_name": "Didier",
  "last_name": "Deschamps",
  "email": null,
  "phone": "+33618276741",
  "mobile_phone": null,
  "company": null,
  "job_title": null,
  "status": "new",
  "source_channel": "ai_agent",
  "source_channel_detail": null,
  "notes": "L'utilisateur, Didier Deschamps, a contacté l'agent Giulia pour l'achat d'un T3 à Saint-Jean-Pied-de-Port et a demandé à être rappelé par son conseiller habituel. [...troncato — riepilogo completo della chiamata, può includere le chiamate precedenti accodate]",
  "tags": ["rent", "ai_agent_lead"],
  "assigned_realtor_external_id": null,
  "interested_property_ids": [],
  "priority": "medium",
  "score": 0,
  "address": null,
  "city": null,
  "postal_code": null,
  "country": null,
  "budget_min": null,
  "budget_max": 8650234,
  "preferred_contact_method": null,
  "next_follow_up_at": null,
  "last_interaction_at": "2026-07-31T14:55:48.316+00:00",
  "sms_consent": false,
  "target_zip_code": "64220",
  "transaction_type": "sale",
  "target_rooms": 3,
  "surface_min_sqm": 20,
  "preferred_area_text": "Saint-Jean-Pied-de-Port",
  "rental_income_3x": null,
  "rental_guarantee_type": "unknown",
  "properties_presented_count": 0,
  "conversion_outcome": "callback_scheduled",
  "visit_datetime_local": null,
  "call_language": "Français",
  "customer_sentiment": "positive",
  "requested_callback": true,
  "disqualifier_reason": "other",
  "custom_fields": {
    "agent_name": "Agence du Pays Basque - Giulia",
    "transaction_type": {
      "value": "sale",
      "rationale": "L'utilisateur a explicitement déclaré vouloir acheter, ce qui correspond à une transaction de type « sale ».",
      "data_collection_id": "transaction_type"
    },
    "requested_callback": {
      "value": true,
      "rationale": "L'utilisateur a explicitement demandé à être rappelé.",
      "data_collection_id": "requested_callback"
    }
  },
  "raw_data": {
    "data_collection_results": {
      "lead_full_name": {
        "value": "Didier Deschamps",
        "rationale": "L'utilisateur a clairement indiqué son nom complet.",
        "data_collection_id": "lead_full_name"
      }
    }
  },
  "provider_created_at": null,
  "provider_updated_at": null,
  "crm_pushed_at": "2026-07-31T12:30:10.829+00:00",
  "created_at": "2026-06-26T16:42:06.236+00:00",
  "updated_at": "2026-07-31T14:55:48.352+00:00"
}

💡 Gli identificativi interni di tracciamento delle chiamate vengono rimossi prima che il payload esca da NoviaMind: non vedrai mai un campo conversation_ids, anche se è una colonna reale del record sottostante.

💡 custom_fields e raw_data.data_collection_results derivano entrambi dall'estrazione dei campi da parte dell'IA e si sovrappongono in gran parte (raw_data è l'estrazione così come è stata catturata in origine; custom_fields è ciò che viene effettivamente salvato sul contatto). Ogni voce riporta value, rationale (perché l'IA ha estratto quel valore) e data_collection_id; alcune voci includono anche un blocco json_schema che descrive il tipo/enum atteso del campo. value è spesso null quando l'IA non ha trovato nulla da estrarre per quel campo durante la chiamata.

property.created / property.updateddata è l'immobile:

{
  "id": "9c8b7a6d-...",
  "team_id": "b81e07c4-...",
  "is_sale": true,
  "is_rent": false,
  "property_type": "apartment",
  "surface_sqm": 65,
  "price_eur": 320000,
  "rooms": 3,
  "address": "12 Rue de Rivoli",
  "city": "Paris",
  "zip_code": "75004",
  "title": "Bright 3-room apartment near the Seine",
  "tags": ["renovated"],
  "created_at": "2026-07-30T14:20:11.000Z",
  "updated_at": "2026-07-30T14:20:11.000Z"
}

realtor.created / realtor.updateddata è l'agente:

{
  "id": "1e2d3c4b-...",
  "team_id": "b81e07c4-...",
  "first_name": "Marc",
  "last_name": "Dupont",
  "email": "[email protected]",
  "phone": "+33698765432",
  "role": "agent",
  "status": "active",
  "license_number": "CPI-1234",
  "service_areas": ["Paris 11e", "Paris 12e"],
  "created_at": "2026-07-30T14:20:11.000Z",
  "updated_at": "2026-07-30T14:20:11.000Z"
}

💡 Le consegne webhook.test (dal pulsante «Invia test») non corrispondono a nessuna delle forme sopra: data è semplicemente { "message": "This is a test webhook delivery from NoviaMind." }.

Verificare la firma

Calcola un HMAC-SHA256 dei byte grezzi del corpo della richiesta (non dell'oggetto ri-serializzato/parsificato: differenze nell'ordine delle chiavi o negli spazi faranno fallire il confronto) con il tuo segreto di firma e confrontalo con l'intestazione X-Webhook-Signature usando un confronto a tempo costante. Esempio Node/Express, supponendo che la route abbia accesso al corpo grezzo (es. express.raw({ type: 'application/json' }) montato solo su questa route, prima di qualsiasi parser del corpo JSON):

const crypto = require('crypto');

function isValidSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader?.startsWith('sha256=')) return false;

  const expectedHex = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const expected = Buffer.from(expectedHex, 'utf8');
  const received = Buffer.from(signatureHeader.slice('sha256='.length), 'utf8');

  // timingSafeEqual solleva un'eccezione se i buffer hanno lunghezze diverse: confronta prima le lunghezze.
  if (expected.length !== received.length) return false;

  return crypto.timingSafeEqual(expected, received);
}

app.post('/webhooks/noviamind', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = isValidSignature(
    req.body,
    req.headers['x-webhook-signature'],
    process.env.WEBHOOK_SECRET,
  );
  if (!valid) return res.status(401).send('invalid signature');

  const event = JSON.parse(req.body); // qui req.body è un Buffer: parsificalo solo dopo la verifica
  // ... gestisci event.event / event.data
  res.sendStatus(200);
});

💡 In n8n, il nodo Webhook ti fornisce il corpo grezzo tramite la sua opzione dati binari/raw: verificalo in un nodo Code con la stessa logica prima di fidarti di event.data, oppure mantieni semplicemente segreto l'URL se la verifica della firma non è necessaria per il tuo caso d'uso.

Il tuo endpoint deve rispondere con uno stato 2xx entro 10 secondi. Qualsiasi altra risposta conta come fallimento.


8.12 Testare un webhook

Clicca su «Invia test» sulla riga di un webhook. NoviaMind invia un evento sintetico webhook.test, firmato esattamente come una consegna reale, e ti mostra lo stato HTTP restituito dal tuo endpoint — ideale per validare il tuo workflow n8n o la verifica della firma prima di andare in produzione.


8.13 Nuovi tentativi, errori e disattivazione automatica

  • Ogni evento viene consegnato con fino a 5 tentativi e backoff esponenziale.
  • La riga del webhook mostra lo stato dell'ultima consegna e il numero di errori consecutivi.
  • Dopo 20 errori consecutivi, il webhook viene disattivato automaticamente così un URL morto smette di consumare tentativi di consegna. Sistema il tuo endpoint, poi clicca su «Riattiva».
  • Qualsiasi consegna riuscita azzera il contatore degli errori.

Cronologia delle consegne

Clicca su «Cronologia» per vedere le ultime consegne (fino a 50, conservate per 30 giorni): data e ora, evento, stato HTTP e numero di tentativo. Vengono conservati solo i metadati dell'esito — mai il contenuto del payload.


8.14 Gestire i webhook

AzioneChi può farlo
Vedere webhook e cronologiaTutti i membri del team
Creare, modificare, testare, attivare/disattivareAmministratori e membri regolari
EliminareSolo amministratori
  • Attiva / Disattiva: sospendi le consegne senza perdere la configurazione (interruttore su ogni riga).
  • Modifica: cambia in qualsiasi momento l'URL, la descrizione o gli eventi sottoscritti. Il segreto di firma non cambia mai.
  • Ruotare il segreto: elimina il webhook e creane uno nuovo.

💡 Gli sviluppatori trovano il riferimento API completo (endpoint, schemi, dettagli della firma) nella documentazione API collegata dalla finestra dei webhook.