Webhook
Riceva una notifica quando accade qualcosa: viene acquisito un lead, un visitatore chiede di parlare con una persona, una conversazione cambia stato. OneType invia un POST firmato al suo endpoint HTTPS.
Stato: Disponibile (Premium). Lo schema di firma qui sotto è definitivo e il codice di verifica in questa pagina è testato con il vettore di test ufficiale di Standard Webhooks.
Configurare un endpoint
In Developer › Webhooks (proprietario o amministratore):
- Aggiunga un URL HTTPS e scelga gli eventi oppure tutti (
*). Facoltativamente scelga Agents: l'endpoint riceverà allora solo gli eventi di quegli agenti e nessun evento a livello di workspace. Se non sceglie alcun agente, riceve gli eventi di ogni agente e quelli a livello di workspace. - Copi il signing secret (
whsec_…). Viene mostrato una sola volta. Potrà ruotarlo in seguito. - Clicchi su Send test per recapitare un
ping, e veda la risposta nel registro delle consegne.
Eventi
| Tipo | Quando |
|---|---|
conversation.created | Un visitatore avvia una conversazione |
conversation.message.created | Un messaggio è completato (visitatore, agente o il suo team) |
conversation.handoff.requested | Il visitatore o l'agente chiede una persona |
conversation.status.changed | Lo stato è cambiato: open, escalated, resolved, unresolved oppure failed (vedere Stati delle conversazioni) |
lead.captured | Un visitatore lascia i suoi dati di contatto |
usage.threshold.reached | Il suo workspace raggiunge il 50%, l'80% o il 100% dei crediti mensili (risposte nei piani con chiave propria) |
csat.submitted | Un visitatore ha valutato una conversazione (una sola volta, definitiva). data.object è un csat_rating con conversation, agent, rating (da 1 a 5), handled_by (ai oppure human), comment (le parole del visitatore, oppure null) e created_at. Contiene il commento, quindi lo tratti come dato personale |
booking.made | Un visitatore ha prenotato un incontro dalla scheda di prenotazione della chat (richiede Cal.com o Calendly, in arrivo). data.object è un booking con event_type, start_at, status, contact, conversation e matched_by (token: prenotato dalla scheda di questa conversazione; email: è stata trovata solo una email verificata, non una prova) |
Nel tempo verranno aggiunti altri tipi di evento. Ignori i tipi che non gestisce. Anche i campi possono essere aggiunti nel tempo alla busta e agli oggetti: ignori quelli che non utilizza.
Payload
{
"id": "evt_01J9…",
"type": "lead.captured",
"created_at": "2026-09-22T10:00:00Z",
"api_version": "v1",
"workspace": "wsp_…",
"agent": "agt_…",
"data": { "object": { "id": "con_…", "object": "contact", "email": "jane@example.com", "name": "Jane" } }
}data.object è lo stesso oggetto che l'API REST restituisce per quella risorsa. Gli oggetti messaggio includono is_ai_generated.
agent è l'id dell'agente a cui appartiene l'evento, nella stessa forma agt_… usata dall'API REST. È presente negli eventi relativi alle conversazioni e ai visitatori di un agente: conversation.created, conversation.message.created, conversation.handoff.requested, conversation.status.changed, csat.submitted, lead.captured e contact.identified. È presente anche in booking.made quando la prenotazione proviene dalla conversazione o dalla persona di un agente. Viene omesso quando non è coinvolto alcun agente: eventi a livello di workspace come usage.threshold.reached, un booking.made che non corrisponde ad alcun agente e modifiche ai contatti che non provengono da un visitatore (per esempio una modifica del suo team). Lo tratti come facoltativo e legga la risorsa da data.object. Il campo è un'aggiunta: gli endpoint esistenti iniziano a riceverlo senza modifiche, quindi un destinatario che rifiuta campi sconosciuti deve consentire agent.
Verifica delle firme
OneType firma ogni consegna con Standard Webhooks, uno schema aperto utilizzato anche da diversi grandi fornitori di API. Ogni richiesta ha tre header:
webhook-id: evt_01J9… (the same on every retry of this event)
webhook-timestamp: 1790000000 (Unix seconds)
webhook-signature: v1,K5oZfz…= (can hold several, space-separated, during secret rotation)La firma è base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), dove key è il suo secret senza il prefisso whsec_ , decodificato da base64.
Sempre:
- verifichi rispetto al corpo grezzo della richiesta, prima di qualsiasi parsing JSON o nuova serializzazione
- rifiuti i timestamp con più di 5 minuti di scarto rispetto a ora, per evitare i replay
- confronti in tempo costante
- accetti qualsiasi firma
v1elencata, così le rotazioni non causano interruzioni
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyOnetypeSignature(rawBody, headers, secret, toleranceSeconds = 300)
{
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures)
{
throw new Error("missing webhook headers");
}
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds)
{
throw new Error("timestamp outside tolerance");
}
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
for (const entry of signatures.split(" "))
{
const [version, value] = entry.split(",");
const candidate = Buffer.from(value ?? "", "base64");
if (version === "v1" && candidate.length === expected.length && timingSafeEqual(candidate, expected))
{
return JSON.parse(rawBody);
}
}
throw new Error("no matching signature");
}
// Express: keep the raw body for this route
app.post("/hooks/onetype", express.raw({ type: "application/json" }), (req, res) => {
let event;
try { event = verifyOnetypeSignature(req.body.toString("utf8"), req.headers, process.env.ONETYPE_WEBHOOK_SECRET); }
catch { return res.sendStatus(400); }
if (event.type === "lead.captured") crm.upsert(event.data.object);
res.sendStatus(204);
});Per instradare per agente, legga event.agent (per esempio if (event.agent === "agt_…") …); è assente negli eventi a livello di workspace. Oppure dia a ogni agente il suo endpoint con Agents (agentIds nell'API): una riconsegna che l'endpoint non accetta più viene rifiutata e un test viene firmato come uno dei suoi agenti.
Python
import base64, hashlib, hmac, json, time
def verify_onetype_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300):
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not msg_id or not timestamp or not signatures:
raise ValueError("missing webhook headers")
if abs(time.time() - int(timestamp)) > tolerance_seconds:
raise ValueError("timestamp outside tolerance")
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for entry in signatures.split(" "):
version, _, value = entry.partition(",")
if version == "v1" and hmac.compare_digest(value, expected):
return json.loads(raw_body)
raise ValueError("no matching signature")
# Flask: request.get_data() is the raw body; request.headers is case-insensitivePHP
<?php
function verifyOnetypeSignature(string $rawBody, array $headers, string $secret, int $toleranceSeconds = 300): array
{
$id = $headers['webhook-id'] ?? null;
$timestamp = $headers['webhook-timestamp'] ?? null;
$signatures = $headers['webhook-signature'] ?? null;
if (!$id || !$timestamp || !$signatures) {
throw new RuntimeException('missing webhook headers');
}
if (abs(time() - (int) $timestamp) > $toleranceSeconds) {
throw new RuntimeException('timestamp outside tolerance');
}
$key = base64_decode(preg_replace('/^whsec_/', '', $secret), true);
$expected = base64_encode(hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true));
foreach (explode(' ', $signatures) as $entry) {
[$version, $value] = array_pad(explode(',', $entry, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $value)) {
return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}
}
throw new RuntimeException('no matching signature');
}
// Usage: $raw = file_get_contents('php://input');
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);Strumenti no-code (Zapier, Make)
I loro catch hook di base accettano qualsiasi richiesta e non possono verificare la firma. Tratti l'URL dell'hook come un segreto. Dove lo strumento offre un passaggio con codice, esegua lì lo stesso controllo: servono il corpo grezzo , i tre header webhook-* e il suo secret. Deduplichi su webhook-id in ogni caso, perché un nuovo tentativo può consegnare un evento due volte. Una guida passo dopo passo per i proprietari è nel centro assistenza: Collegare Zapier o Make.
Verifichi la sua implementazione con il vettore di test di Standard Webhooks:
- secret
whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw - id
msg_p5jXN8AQM9LWM0D4loKWxJek - timestamp
1614265330 - body
{"test": 2432232314} - risultato atteso
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Risposta e nuovi tentativi
- Risponda con un qualsiasi 2xx entro 10 secondi. Esegua le operazioni lente dopo aver risposto, per esempio mettendole in coda.
- Con una risposta diversa da 2xx o un timeout, riproviamo nell'arco di circa 3 giorni: dopo 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h, poi ogni 24 h per tre volte.
- La consegna avviene almeno una volta. Memorizzi
webhook-ide ignori le ripetizioni. - L'ordine non è garantito. Usi
created_atquando l'ordine è importante. - Un endpoint che fallisce per 3 giorni di fila viene disattivato, e i proprietari vedono un banner. Lo riattivi nella dashboard; da quel momento i nuovi eventi arrivano.
- Il registro delle consegne (30 giorni) mostra ogni tentativo con il suo stato e i tempi. Redeliver invia di nuovo un evento.
- Consegniamo solo a indirizzi HTTPS pubblici e non seguiamo i reindirizzamenti.