Manuale API e webhook

Due strade, per cose diverse. L’API serve quando sei tu ad andare a prendere i dati. Gli avvisi in uscita servono quando vuoi che sia Swavo ad avvisarti nell’istante in cui succede qualcosa — è quella giusta se i contatti devono finire nel tuo CRM.

1. La chiave

Serve il piano Pro o superiore. Sul Free e sullo Start l’API e gli avvisi in uscita non ci sono: il pannello non fa creare la chiave, e una chiave nata quando il piano era piu' alto smette di rispondere (403).

Si crea dal portale: Impostazioni → Integrazioni → API Keys → Nuova chiave. Si vede una volta sola, quindi va copiata subito. Vale solo per la tua azienda: non può leggere i dati di nessun altro. Si revoca quando vuoi, dallo stesso pannello.

Va messa nell’intestazione x-api-key di ogni richiesta. Base: https://portal.swavo.ai

curl https://portal.swavo.ai/api/v1/customers?limit=5 \
  -H "x-api-key: sk_live_..."

C’è un limite di chiamate per chiave: oltre quello arriva un 429 con il momento in cui riparte. La chiave serve solo per l’API: gli avvisi in uscita non la usano, hanno un segreto loro.

2. Ricette: n8n, Make, Zapier

Non serve installare un connettore Swavo, e non serve farselo sviluppare: tutti e tre hanno già i mattoni giusti. Sotto, i passi esatti.

n8n

  1. Nuovo workflow → nodo Webhook. Metti HTTP Method: POST e Respond: Immediately — così n8n risponde subito e noi non restiamo ad aspettare il resto del tuo flusso.
  2. Copia la Production URL, non la Test URL: quella vive solo mentre tieni aperto l’editor, e il tuo primo contatto vero arriverebbe nel vuoto.
  3. Nel portale Swavo: Impostazioni → Integrazioni → Avvisi in uscita → Nuova destinazione. Incolla l’indirizzo, spunta gli eventi, salva. Copia il segreto whsec_… che compare: si vede una volta sola.
  4. Attiva il workflow, poi torna in Swavo e premi Prova: in n8n deve comparire un’esecuzione con dentro event: "webhook.test".
  5. Aggiungi un nodo Code subito dopo il Webhook per verificare la firma (codice al punto 4) e un IF che scarta quello che non passa.
  6. Da lì collega quello che vuoi: il tuo CRM, Google Sheets, un’email. I campi del contatto stanno sotto body.data.

Per andare a prendere i dati invece: nodo HTTP Request, metodo GET, URL https://portal.swavo.ai/api/v1/tickets, e fra gli Headers aggiungi x-api-key. Conviene salvarla come credenziale Header Auth, così non gira in chiaro dentro il workflow.

Make

  1. Nuovo scenario → primo modulo Webhooks → Custom webhook → Add. Dagli un nome e copia l’indirizzo.
  2. Incollalo in Swavo come sopra, scegli gli eventi, salva, copia il segreto.
  3. In Make premi Re-determine data structure: resta in ascolto. Torna in Swavo, premi Prova, e Make impara da solo la forma dei dati.
  4. Aggiungi i moduli che ti servono. I campi sono sotto data: data.name, data.phone, data.reason.
  5. Per la firma: modulo Tools → Set variable con la funzione sha256(…; "hex"; segreto) di Make, e un Router con un filtro che lascia passare solo se coincide.
  6. Salva e metti lo scenario su ON: un webhook con lo scenario spento non riceve niente.

Per leggere: modulo HTTP → Make a request, GET, con l’intestazione x-api-key.

Zapier

  1. Nuovo Zap → trigger Webhooks by Zapier → Catch Hook. Richiede un piano a pagamento: è l’unico dei tre in cui serve.
  2. Copia l’URL che ti dà, incollalo in Swavo, scegli gli eventi, salva.
  3. Premi Prova in Swavo, poi Test trigger in Zapier: deve trovare il messaggio.
  4. Collega le azioni. Zapier appiattisce i campi annidati: il nome del contatto lo trovi come data__name, il telefono come data__phone.
  5. La verifica della firma richiede uno step Code by Zapier. Se non lo usi, tieni segreto l’URL del Catch Hook: chi lo conosce può inventarti un contatto.

3. Specifiche degli avvisi

Gli eventi che puoi ricevere:

lead.createdUn contatto nuovo da richiamare: qualcuno ha lasciato un recapito.
ticket.createdUna richiesta di assistenza aperta, lead o no.
appointment.createdUn appuntamento preso.

L’involucro è sempre lo stesso, per tutti gli eventi: impari una forma sola. Quello che cambia è data.

POST <il tuo indirizzo>
Content-Type: application/json
User-Agent: Swavo-Webhooks/1.0
Swavo-Event: lead.created
Swavo-Delivery: 6f1c…                      <- id univoco di QUESTA consegna
Swavo-Signature: t=1757500000,v1=9ab3…
Idempotency-Key: lead.created:<id>:<dest>  <- identica a ogni ritentativo

{
  "id": "6f1c…",                  // = Swavo-Delivery
  "event": "lead.created",
  "created_at": "2026-09-10T14:22:05.120Z",
  "company_id": "…",              // la tua azienda, sempre la stessa
  "attempt": 1,                   // 1 al primo colpo, poi 2, 3…
  "data": { … }                   // dipende dall'evento
}

data di lead.created — è l’evento che interessa quasi sempre:

ticket_iduuidLa richiesta da cui nasce il contatto.
customer_iduuid | nullLa scheda cliente, se il numero era già censito.
namestring | nullIl nome, ripulito: prima la scheda cliente, poi quello detto al telefono.
phonestring | nullIl recapito in formato internazionale. Senza recapito non è un lead.
reasonstringCosa voleva, con le parole dell’assistente, senza i tag interni.
channelstringtelefono · whatsapp · sito · portale · altro
lead_statusstringLa fase del funnel del tenant. La prima è tipicamente «da richiamare».
prioritystringcritical · high · normal · low
statusstringnew · assigned · in_progress · completed · cancelled · pending_parts
sourcestring | nullDa dove è arrivato, in forma grezza (phone_ai, whatsapp, api, portal…).
created_atISO 8601Quando è nato il contatto.

ticket.created e appointment.created portano invece l’oggetto grezzo, con gli stessi nomi di campo che vedi chiamando GET /api/v1/tickets e GET /api/v1/calendar/events. È la regola generale: il webhook ti dà lo stesso oggetto dell’API.

Cosa ci aspettiamo da te, e cosa facciamo noi:

  • Rispondi 2xx e per noi è consegnato. Rispondi entro 10 secondi: oltre, chiudiamo e contiamo un fallimento. Metti il lavoro in coda da te, non farci aspettare.
  • 5xx, timeout, 429 o 408 → riproviamo dopo 5, 10, 20 e 40 minuti, per un massimo di 5 tentativi. Poi la consegna muore e resta visibile come errore nel pannello.
  • Ogni altro 4xx → non ritentiamo. Ci stai dicendo che indirizzo o formato sono sbagliati: fra quaranta minuti sarebbe identico.
  • 3xx → è un fallimento. Non seguiamo i redirect: metti l’indirizzo finale.
  • Lo stesso fatto può arrivare due volte. Succede in ogni sistema del genere. Usa Idempotency-Key, che è identica a ogni ritentativo dello stesso evento: se l’hai già vista, buttala.
  • L’ordine non è garantito. Se ti serve, ordina per data.created_at, non per ordine di arrivo.
  • Solo https, e mai verso indirizzi di rete privata: controllato quando registri e di nuovo a ogni consegna.

4. Verificare la firma

Alla registrazione ricevi un segreto (whsec_…), mostrato una volta sola. Ogni richiesta porta Swavo-Signature nella forma t=<secondi>,v1=<firma>, dove la firma è l’HMAC-SHA256 di <t>.<corpo grezzo>.

Due trappole: firma il corpo così com’è arrivato, prima di trasformarlo in oggetto — ri-serializzarlo cambia gli spazi e la firma non torna; e rifiuta quello che è più vecchio di 5 minuti, altrimenti un messaggio vero intercettato si può rispedire per sempre.

// Node — e va tale e quale dentro un nodo Code di n8n
import crypto from 'crypto';

function verifica(corpoGrezzo, intestazione, segreto) {
  const p = Object.fromEntries(
    intestazione.split(',').map(x => x.split('='))
  );
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;

  const atteso = crypto
    .createHmac('sha256', segreto)
    .update(`${p.t}.${corpoGrezzo}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(atteso, 'hex'),
    Buffer.from(p.v1, 'hex')
  );
}
# Python
import hmac, hashlib, time

def verifica(corpo_grezzo: bytes, intestazione: str, segreto: str) -> bool:
    p = dict(x.split("=", 1) for x in intestazione.split(","))
    if abs(time.time() - int(p["t"])) > 300:
        return False
    atteso = hmac.new(
        segreto.encode(),
        f'{p["t"]}.'.encode() + corpo_grezzo,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(atteso, p["v1"])

5. L’API

GET /api/v1/customersElenco clienti, paginato e ricercabile.
POST /api/v1/customersCrea un cliente.
GET /api/v1/ticketsElenco richieste di assistenza.
POST /api/v1/ticketsApri una richiesta.
GET /api/v1/techniciansOperatori e tecnici.
GET /api/v1/calendar/eventsAppuntamenti a calendario.
GET /api/v1/catalog/price-listsListini e servizi.
GET /api/v1/inventoryMagazzino.

Le risposte hanno sempre la stessa forma: { "success": true, "data": … } oppure { "success": false, "error": { "code", "message" } }. Gli elenchi accettano limit, offset, search, sort_by, sort_order, e riportano il totale accanto ai dati.

6. Provare prima del danno

Accanto a ogni destinazione c’è Prova: bussa subito al tuo indirizzo con un messaggio finto (webhook.test, che non esiste fra gli eventi sottoscrivibili proprio perché non deve arrivare per caso) e ti dice cosa ha risposto e in quanti millisecondi. Nella stessa riga trovi l’ultima consegna riuscita e l’ultimo errore, scritto in chiaro.

Usala prima: un indirizzo sbagliato scoperto quando non arriva il primo contatto vero è una persona persa.

7. Cosa non c’è ancora

Sta qui e non sulla pagina commerciale, per scelta: i limiti servono a chi deve costruire davvero, e non voglio che li scopra a metà lavoro.

  • Nessuna scheda Swavo nei cataloghi di Make e Zapier. Il collegamento si fa a mano come sopra, e funziona uguale.
  • Nessuna sincronizzazione preconfezionata verso CRM specifici (HubSpot, Salesforce, Pipedrive). Si costruisce sugli avvisi in uscita: mezza giornata di lavoro, non un progetto.
  • Solo fatti nuovi. Oggi escono creazioni: lead.created, ticket.created, appointment.created. Non esistono ancora gli eventi di modifica o cancellazione — se ti serve «sposta l’appuntamento», chiedicelo: aggiungerne uno è una riga.
  • Nessuna riconsegna a richiesta. Una consegna fallita definitivamente non si può ancora rilanciare dal pannello: si vede l’errore, e si riparte dall’API.
  • Nessun ordine garantito e nessun raggruppamento. Un evento, una richiesta: non mandiamo lotti.