Webhooks
Soyez averti quand quelque chose se produit : un lead est capturé, un visiteur demande un humain, une conversation change de statut. OneType envoie un POST signé à votre point de terminaison HTTPS.
Statut : Available (Premium). Le schéma de signature ci-dessous est définitif, et le code de vérification de cette page est testé avec le vecteur de test officiel de Standard Webhooks.
Configurer un point de terminaison
Dans Developer › Webhooks (propriétaire ou admin) :
- Ajoutez une URL HTTPS et choisissez les événements, ou tous (
*). Choisissez éventuellement Agents : le point de terminaison ne reçoit alors que les événements de ces agents, et aucun événement à l'échelle de l'espace de travail. Sans agent choisi, il reçoit les événements de tous les agents ainsi que ceux de l'espace de travail. - Copiez le secret de signature (
whsec_…). Il n'est affiché qu'une seule fois. Vous pouvez le renouveler plus tard. - Cliquez sur Send test pour livrer un
ping, et consultez la réponse dans le journal des livraisons.
Événements
| Type | Quand |
|---|---|
conversation.created | Un visiteur démarre une conversation |
conversation.message.created | Un message est terminé (visiteur, agent ou votre équipe) |
conversation.handoff.requested | Le visiteur ou l'agent demande un humain |
conversation.status.changed | Le statut a changé : open, escalated, resolved, unresolved ou failed (voir Statuts de conversation) |
lead.captured | Un visiteur laisse ses coordonnées |
usage.threshold.reached | Votre espace de travail atteint 50 %, 80 % ou 100 % de ses crédits mensuels (réponses sur les offres avec clé propre) |
csat.submitted | Un visiteur a noté une conversation (une seule fois, définitive). data.object est un csat_rating avec conversation, agent, rating (de 1 à 5), handled_by (ai ou human), comment (les propres mots du visiteur, ou null) et created_at. Il contient le commentaire, donc traitez-le comme des données personnelles |
booking.made | Un visiteur a réservé un rendez-vous depuis la carte de réservation du chat (nécessite Cal.com ou Calendly, bientôt disponible). data.object est un booking avec event_type, start_at, status, contact, conversation et matched_by (token : réservé depuis la carte de cette conversation ; email : seul un e-mail vérifié a correspondu, ce n'est pas une preuve) |
D'autres types d'événements seront ajoutés au fil du temps. Ignorez les types que vous ne gérez pas. Des champs peuvent aussi être ajoutés à l'enveloppe et aux objets au fil du temps : ignorez ceux que vous n'utilisez pas.
Charge utile
{
"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 est le même objet que celui renvoyé par l'API REST pour cette ressource. Les objets message incluent is_ai_generated.
agent est l'identifiant de l'agent auquel appartient l'événement, dans la même forme agt_… que celle qu'utilise l'API REST. Il est présent sur les événements concernant les conversations et les visiteurs d'un agent : conversation.created, conversation.message.created, conversation.handoff.requested, conversation.status.changed, csat.submitted, lead.captured et contact.identified. Il figure aussi sur booking.made lorsque la réservation provient de la conversation ou de la personne d'un agent. Il est omis quand aucun agent n'est concerné : événements à l'échelle de l'espace de travail tels que usage.threshold.reached, un booking.made qui ne correspond à aucun agent, et modifications de contact qui ne viennent pas d'un visiteur (par exemple une modification par votre équipe). Traitez-le comme facultatif et lisez la ressource depuis data.object. Le champ est un ajout : les points de terminaison existants commencent à le recevoir sans aucun changement, donc un récepteur qui rejette les champs inconnus doit autoriser agent.
Vérifier les signatures
OneType signe chaque livraison avec Standard Webhooks, un schéma ouvert utilisé aussi par plusieurs grands fournisseurs d'API. Chaque requête comporte trois en-têtes :
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 signature est base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), où key est votre secret sans le préfixe whsec_ , décodé en base64.
Toujours :
- vérifier sur le corps brut de la requête, avant tout parsing ou toute re-sérialisation JSON
- rejeter les horodatages à plus de 5 minutes de l'instant présent, ce qui empêche les rejeux
- comparer en temps constant
- accepter toute signature
v1listée, afin que les rotations ne vous posent jamais de problème
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);
});Pour router par agent, lisez event.agent (par exemple if (event.agent === "agt_…") …) ; il est absent des événements à l'échelle de l'espace de travail. Ou donnez à chaque agent son propre point de terminaison avec Agents (agentIds dans l'API) : une nouvelle livraison que le point de terminaison n'accepte plus est refusée, et un test est signé comme l'un de ses agents.
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);Outils no-code (Zapier, Make)
Leurs hooks de capture basiques acceptent n'importe quelle requête et ne peuvent pas vérifier la signature. Traitez l'URL du hook comme un secret. Si l'outil propose une étape de code, effectuez-y la même vérification : il lui faut le corps brut , les trois en-têtes webhook-* et votre secret. Dédoublonnez sur webhook-id dans tous les cas, car une nouvelle tentative peut livrer un événement deux fois. Un guide pas à pas pour les propriétaires se trouve dans le centre d'aide : Connecter Zapier ou Make.
Vérifiez votre implémentation avec le vecteur de test de Standard Webhooks :
- secret
whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw - id
msg_p5jXN8AQM9LWM0D4loKWxJek - horodatage
1614265330 - corps
{"test": 2432232314} - résultat attendu
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Réponse et nouvelles tentatives
- Répondez par n'importe quel 2xx dans les 10 secondes. Faites le travail lent après avoir répondu, par exemple en le mettant en file d'attente.
- En cas de réponse non 2xx ou de délai dépassé, nous réessayons pendant environ 3 jours : après 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h, puis toutes les 24 h, trois fois.
- La livraison est au moins une fois. Conservez
webhook-idet ignorez les répétitions. - L'ordre n'est pas garanti. Utilisez
created_atquand l'ordre compte. - Un point de terminaison qui n'échoue que pendant 3 jours est désactivé, et les propriétaires voient une bannière. Réactivez-le dans le tableau de bord ; les nouveaux événements circulent à partir de là.
- Le journal des livraisons (30 jours) montre chaque tentative avec son statut et sa durée. Redeliver renvoie un événement.
- Nous ne livrons qu'à des adresses HTTPS publiques et ne suivons pas les redirections.