Audin Docs
API REST (partner)

Outbound Webhooks

Ricevi notifiche in tempo reale quando un lead viene creato, aggiornato o eliminato in Audin — eventi firmati HMAC, consegnati con retry durabile verso i tuoi endpoint HTTPS.

Versione Markdown di questa pagina

Scarica o copia il contenuto di questa pagina in formato .md — utile per fornirlo a un agente AI che integra questa specifica feature.

Scarica .md

Gli outbound webhook sono il canale con cui Audin notifica il tuo sistema quando qualcosa cambia nel CRM. Tu configuri uno o più endpoint HTTPS, scegli i topic a cui iscriverti (es. lead.created, lead.updated, lead.deleted) e Audin ti recapita un POST firmato ogni volta che l'evento si verifica.

È il complemento speculare dell'ingestion webhook: l'ingestion serve a spingere i cambiamenti dal tuo sistema verso Audin; gli outbound webhook servono a ricevere da Audin i cambiamenti avvenuti nel CRM (anche quelli originati da una chiamata, da un workflow o da un operatore dentro Audin).

I webhook sono notifiche, non una fonte di verità. La consegna e i retry sono durabili, ma in casi limite (es. un riavvio nel preciso istante tra la scrittura e l'accodamento) una singola notifica può non partire. Per uno stato sempre coerente, riconcilia periodicamente via API REST (GET /leads) oltre ad ascoltare i webhook.

Configurare un endpoint

Ogni endpoint ha: un URL HTTPS di destinazione, un flag enabled, un set di topic sottoscritti, eventuali header personalizzati (inviati con ogni richiesta) e un signing secret generato da Audin.

Dal dashboard

In Impostazioni → Webhook (https://app.audin.ai/settings/webhooks):

  1. Crea un endpoint con l'URL HTTPS pubblico del tuo ricevitore.
  2. Seleziona i topic a cui iscriverti.
  3. Copia il signing secret mostrato una sola volta alla creazione (serve per verificare le firme — vedi sotto).
  4. (Opzionale) Usa Test connection per inviare subito un evento di prova (topic webhook.test, id con prefisso evt_test-): non viene salvato nello storico ed è pensato per validare che il tuo endpoint risponda.

Nella stessa pagina trovi lo storico delle consegne (esito, status code, tentativo, durata) per diagnosticare un endpoint che fallisce.

Via API

Gli stessi endpoint sono gestibili via API REST con la tua Account API Key (header X-API-Key, la stessa di tutta l'API partner).

# Elenco dei topic disponibili
curl https://api.audin.ai/webhook-topics \
  -H "X-API-Key: $API_KEY"

# Crea un endpoint iscritto ai topic dei lead
curl -X POST https://api.audin.ai/webhook-endpoints \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/audin",
    "enabled": true,
    "description": "Sync CRM esterno",
    "customHeaders": [{ "name": "X-My-Token", "value": "abc123" }],
    "topics": ["lead.created", "lead.updated", "lead.deleted"]
  }'

La risposta alla creazione include plainSecret — il signing secret in chiaro, mostrato una sola volta. Conservalo lato server.

OperazioneEndpoint
Topic disponibiliGET /webhook-topics
Lista / crea endpointGET · POST /webhook-endpoints
Dettaglio / modifica / eliminaGET · PUT · DELETE /webhook-endpoints/{id}
Aggiorna le sottoscrizioniPUT /webhook-endpoints/{id}/subscriptions (rimpiazza l'intero set)
Ruota il secretPOST /webhook-endpoints/{id}/rotate-secret
Invia un evento di provaPOST /webhook-endpoints/{id}/test
Storico consegneGET /webhook-endpoints/{id}/deliveries

Topic disponibili

I topic seguono la forma <entità>.<azione>. Nella versione corrente:

TopicQuando
lead.createdUn nuovo lead viene creato (da qualsiasi canale: API, import, chiamata, workflow).
lead.updatedUn campo visibile del lead cambia (incluso il cambio di stato/stage).
lead.deletedUn lead viene eliminato.
comment.createdUn nuovo commento/nota viene aggiunto (su un lead, un deal o un'attività). Include anche i commenti generati automaticamente da Audin (es. riepiloghi di chiamata, note di workflow).
comment.updatedIl contenuto di un commento cambia.
comment.deletedUn commento viene eliminato.

Il catalogo cresce un'entità alla volta. Interroga GET /webhook-topics per l'elenco aggiornato senza dover consultare la documentazione.

Formato dell'evento (v1)

Ogni consegna è un POST con un corpo JSON in questa forma:

CampoTipoNote
idstringIdentificativo univoco dell'evento (evt_<uuid>). Chiave di idempotency lato tuo (vedi sotto).
topicstringIl topic dell'evento, es. lead.updated.
account_idstringAccount a cui si riferisce l'evento.
created_atstring (ISO 8601)Istante di generazione dell'evento.
versionstringVersione del formato ("1").
data.objectobjectSnapshot dell'entità interessata.
data.previous_attributesobjectSolo su *.updated: i campi cambiati con il valore precedente.

Esempio — lead.updated

Su un aggiornamento, previous_attributes contiene solo i campi cambiati, con il valore che avevano prima; object è lo snapshot dopo la modifica:

{
  "id": "evt_9b1c2d3e-4f5a-6789-abcd-ef0123456789",
  "topic": "lead.updated",
  "account_id": "51358416-…",
  "created_at": "2026-06-18T10:30:00.000Z",
  "version": "1",
  "data": {
    "object": {
      "id": "f1e2d3c4-…",
      "fullName": "Mario Rossi",
      "firstName": "Mario",
      "lastName": "Rossi",
      "jobTitle": "Direttore acquisti",
      "email": "mario@acme.example.com",
      "phoneNumber": "+393334445566",
      "city": "Milano",
      "leadStatus": "QUALIFIED",
      "stageId": "stage-2-uuid"
    },
    "previous_attributes": {
      "leadStatus": "NEW",
      "stageId": "stage-1-uuid"
    }
  }
}

Esempio — lead.created

Sugli eventi *.created e *.deleted non è presente previous_attributes:

{
  "id": "evt_1a2b3c4d-5e6f-7890-abcd-ef1234567890",
  "topic": "lead.created",
  "account_id": "51358416-…",
  "created_at": "2026-06-18T09:00:00.000Z",
  "version": "1",
  "data": {
    "object": {
      "id": "a9b8c7d6-…",
      "fullName": "Giulia Bianchi",
      "firstName": "Giulia",
      "lastName": "Bianchi",
      "jobTitle": "Responsabile marketing",
      "email": "giulia@acme.example.com",
      "phoneNumber": "+393331112233",
      "city": "Torino",
      "leadStatus": "NEW"
    }
  }
}

Lead sincronizzati da un'integrazione (es. HubSpot)

Per i lead che arrivano da un'integrazione come HubSpot, lo snapshot object include anche i campi nativi mappati 1:1 su Audin — firstName, lastName, jobTitle, mobilePhone, companyName, website, lifecycleStage, address, city, state, zip, country — valorizzati quando la sincronizzazione li popola (altrimenti null).

Le proprietà personalizzate definite nel tuo account HubSpot e le altre proprietà native non mappate non vanno perse: sono annidate sotto customParams.hubspot dentro lo stesso object:

{
  "object": {
    "id": "a9b8c7d6-…",
    "fullName": "Giulia Bianchi",
    "firstName": "Giulia",
    "lastName": "Bianchi",
    "lifecycleStage": "lead",
    "city": "Torino",
    "customParams": {
      "hubspot": {
        "native": { "hs_object_id": "12345", "hs_analytics_source": "ORGANIC_SEARCH" },
        "custom": { "settore_interesse": "Ortodonzia", "codice_cliente": "MD-0099" }
      }
    }
  }
}

Nessun campo è nascosto. Tutti i campi nativi e l'intero blocco customParams.hubspot (native + custom) sono inclusi di default nello snapshot degli eventi lead.created / lead.updated. Scrivi il tuo ricevitore in modo da ignorare le chiavi che non ti interessano.

Esempio — comment.created

I commenti (note su lead, deal o attività) seguono lo stesso envelope. In object trovi il commento; metadata e i riferimenti interni di workflow non sono inclusi nello snapshot:

{
  "id": "evt_2b3c4d5e-6f70-8901-bcde-f23456789012",
  "topic": "comment.created",
  "account_id": "51358416-…",
  "created_at": "2026-06-18T11:15:00.000Z",
  "version": "1",
  "data": {
    "object": {
      "id": "c4d5e6f7-…",
      "content": "Richiamare il cliente lunedì mattina.",
      "type": "NOTE",
      "authorType": "USER",
      "userId": "a9b8c7d6-…",
      "dealId": null,
      "createdAt": "2026-06-18T11:15:00.000Z"
    }
  }
}

Anche i commenti generati da Audin (riepiloghi di chiamata, note prodotte da un workflow) emettono comment.created: in quei casi authorType non è USER. Se ti interessano solo le note umane, filtra sul valore di authorType nel tuo handler.

Il wire format v1 è un contratto. Un cambiamento incompatibile comporterebbe un nuovo version. Scrivi il tuo ricevitore in modo da ignorare campi sconosciuti dentro object (additivi) senza romperti.

Header della richiesta

Ogni POST include questi header (oltre agli eventuali header personalizzati che hai configurato):

HeaderSignificato
X-Audin-SignatureFirma HMAC-SHA256 nel formato v1=<hex>.
X-Audin-Signature-TimestampTimestamp Unix (secondi) usato per calcolare la firma.
X-Audin-Webhook-IdIdentificativo dell'endpoint di destinazione.
X-Audin-Delivery-IdIdentificativo del singolo tentativo di consegna.
X-Audin-TopicIl topic dell'evento.
User-AgentAudin-Webhook/1.0.

Verificare la firma

Verifica sempre la firma prima di fidarti del payload. La firma è calcolata come:

firma = HMAC_SHA256( signing_secret , "<timestamp>.<corpo_grezzo>" )

dove <timestamp> è il valore dell'header X-Audin-Signature-Timestamp e <corpo_grezzo> è il body della richiesta esattamente come ricevuto (la stringa raw, non un JSON ri-serializzato). Il risultato è esadecimale e nell'header arriva con il prefisso v1=.

Confronta in tempo costante e rifiuta le richieste con timestamp troppo vecchio (protezione anti-replay).

const crypto = require("crypto");

// Usa il body GREZZO della richiesta (Buffer/stringa), NON un JSON già parsato.
function verifyAudinWebhook(rawBody, headers, signingSecret, toleranceSec = 300) {
  const signature = headers["x-audin-signature"];          // "v1=<hex>"
  const timestamp = headers["x-audin-signature-timestamp"]; // "1718705400"

  if (!signature || !timestamp) return false;

  // 1. Anti-replay: scarta timestamp troppo vecchi (o nel futuro).
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > toleranceSec) return false;

  // 2. Ricalcola la firma sul payload firmato "<timestamp>.<rawBody>".
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected =
    "v1=" +
    crypto.createHmac("sha256", signingSecret).update(signedPayload).digest("hex");

  // 3. Confronto in tempo costante (le lunghezze devono coincidere).
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Molti framework espongono solo il body già parsato (req.body come oggetto). Per la verifica ti serve il body grezzo: in Express, ad esempio, express.json({ verify: (req, _res, buf) => { req.rawBody = buf } }). Ri-serializzare il JSON parsato può alterare spazi/ordine delle chiavi e far fallire la verifica.

Consegna, retry e risposta attesa

Il tuo endpoint deve rispondere con uno status 2xx il più velocemente possibile (entro 10 secondi). Accoda il lavoro e rispondi subito: non elaborare in modo sincrono dentro la richiesta del webhook.

  • Non-2xx o timeout → Audin ritenta secondo questa pianificazione: 1m → 5m → 30m → 2h → 12h → 24h (fino a 6 tentativi su una finestra di circa 40 ore). Esauriti i tentativi, la consegna è marcata come fallita.
  • 410 Gone → Audin smette di ritentare quella consegna. Usalo se l'endpoint non deve più ricevere quel tipo di evento.
  • Ogni tentativo è tracciato nello storico consegne (status code, durata, eventuale errore).

Idempotency

La stessa consegna può arrivare più di una volta (es. il tuo 2xx non raggiunge Audin e scatta un retry). Inoltre, un singolo aggiornamento che cambia sia lo stato sia altri campi visibili può generare due eventi lead.updated con previous_attributes disgiunti.

Deduplica sempre sull'id dell'evento (evt_…): tienilo come chiave e ignora gli id già processati. Tratta ogni handler come idempotente.

Rotazione del secret

Puoi rigenerare il signing secret di un endpoint dal dashboard ("Rigenera") o via POST /webhook-endpoints/{id}/rotate-secret. Alla rotazione:

  • Audin inizia a firmare con il nuovo secret. C'è una finestra di grazia di ~24h sul secret precedente, pensata per darti il tempo di aggiornare la verifica.
  • In un deployment distribuito, può servire fino a ~60 secondi perché il nuovo secret venga usato ovunque: durante quell'intervallo alcune consegne potrebbero ancora arrivare firmate col secret precedente.

Linea guida pratica: durante una rotazione, fai accettare al tuo verificatore sia il vecchio sia il nuovo secret per un breve periodo (la finestra di 24h copre ampiamente il rollout), poi rimuovi il vecchio.

// Durante la rotazione, accetta entrambi i secret.
function verifyWithRotation(rawBody, headers, secrets /* [nuovo, vecchio] */) {
  return secrets.some((s) => verifyAudinWebhook(rawBody, headers, s));
}

Prossimo passo

On this page