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
- Nuovo workflow → nodo Webhook. Metti
HTTP Method: POSTeRespond: Immediately— così n8n risponde subito e noi non restiamo ad aspettare il resto del tuo flusso. - 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.
- 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. - Attiva il workflow, poi torna in Swavo e premi Prova: in n8n deve comparire un’esecuzione con dentro
event: "webhook.test". - 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.
- 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
- Nuovo scenario → primo modulo Webhooks → Custom webhook → Add. Dagli un nome e copia l’indirizzo.
- Incollalo in Swavo come sopra, scegli gli eventi, salva, copia il segreto.
- In Make premi Re-determine data structure: resta in ascolto. Torna in Swavo, premi Prova, e Make impara da solo la forma dei dati.
- Aggiungi i moduli che ti servono. I campi sono sotto
data:data.name,data.phone,data.reason. - 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. - 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
- Nuovo Zap → trigger Webhooks by Zapier → Catch Hook. Richiede un piano a pagamento: è l’unico dei tre in cui serve.
- Copia l’URL che ti dà, incollalo in Swavo, scegli gli eventi, salva.
- Premi Prova in Swavo, poi Test trigger in Zapier: deve trovare il messaggio.
- Collega le azioni. Zapier appiattisce i campi annidati: il nome del contatto lo trovi come
data__name, il telefono comedata__phone. - 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.created | Un contatto nuovo da richiamare: qualcuno ha lasciato un recapito. |
| ticket.created | Una richiesta di assistenza aperta, lead o no. |
| appointment.created | Un 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_id | uuid | La richiesta da cui nasce il contatto. |
| customer_id | uuid | null | La scheda cliente, se il numero era già censito. |
| name | string | null | Il nome, ripulito: prima la scheda cliente, poi quello detto al telefono. |
| phone | string | null | Il recapito in formato internazionale. Senza recapito non è un lead. |
| reason | string | Cosa voleva, con le parole dell’assistente, senza i tag interni. |
| channel | string | telefono · whatsapp · sito · portale · altro |
| lead_status | string | La fase del funnel del tenant. La prima è tipicamente «da richiamare». |
| priority | string | critical · high · normal · low |
| status | string | new · assigned · in_progress · completed · cancelled · pending_parts |
| source | string | null | Da dove è arrivato, in forma grezza (phone_ai, whatsapp, api, portal…). |
| created_at | ISO 8601 | Quando è 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/customers | Elenco clienti, paginato e ricercabile. |
| POST /api/v1/customers | Crea un cliente. |
| GET /api/v1/tickets | Elenco richieste di assistenza. |
| POST /api/v1/tickets | Apri una richiesta. |
| GET /api/v1/technicians | Operatori e tecnici. |
| GET /api/v1/calendar/events | Appuntamenti a calendario. |
| GET /api/v1/catalog/price-lists | Listini e servizi. |
| GET /api/v1/inventory | Magazzino. |
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.