Webhooks
Plutôt que d’interroger l’API, abonnez une URL de votre serveur : Vizalia y envoie un POST à chaque étape d’une session, de l’invitation au rapport.
S’abonner
Section intitulée « S’abonner »Un abonnement, c’est une URL HTTPS publique et les événements qu’elle reçoit (* pour tous). Le secret de signature, whsec_…, n’est renvoyé qu’une fois, dans la réponse de la création.
curl https://api.vizalia.ai/v1/webhook_endpoints \ -H "Authorization: Bearer $VIZALIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"url": "https://crm.exemple.fr/webhooks/vizalia", "events": ["session.ended", "report.ready"], "description": "Rapport dans le ticket"}'L’adresse doit être en https, sur le port 443, sans identifiants (https://utilisateur:motdepasse@…), vers un hôte public : une adresse privée (réseau interne, localhost) est refusée avec 422 invalid_webhook_url. Un espace a au plus 16 abonnements par mode, production et test : un abonnement créé avec une clé de test (livemode: false) reçoit les événements des sessions de test. La clé doit avoir la portée webhooks:manage.
Recevoir et vérifier
Section intitulée « Recevoir et vérifier »Les webhooks suivent la spécification
Standard Webhooks :
un POST JSON avec les en-têtes webhook-id, webhook-timestamp et webhook-signature. Vérifiez
chaque envoi sur le corps brut, avant de le lire, avec la bibliothèque officielle de votre langage
(paquet standardwebhooks en Node.js, Python, Ruby, PHP, Go, Java et d’autres) et le secret de
l’abonnement :
import { Webhook } from "standardwebhooks";
// rawBody : le corps reçu, octet pour octet ; headers : les en-têtes de la requêteconst event = new Webhook(process.env.VIZALIA_WEBHOOK_SECRET).verify(rawBody, headers);import osfrom standardwebhooks import Webhook
event = Webhook(os.environ["VIZALIA_WEBHOOK_SECRET"]).verify(raw_body, headers)Répondez 2xx en moins de 15 secondes, puis traitez l’événement. webhook-id reste le même d’une
tentative à l’autre : gardez-le pour ignorer un doublon. Les envois arrivent au moins une fois, sans
ordre garanti ; en cas de doute, relisez la session. GET /events rattrape un envoi manqué.
Le détail de la vérification, avec ou sans bibliothèque, est sur la page Vérifier la signature.
Les événements
Section intitulée « Les événements »| Événement | Quand | data |
|---|---|---|
session.created | Session créée Une session a été créée dans l'espace, quelle que soit son origine (API, console, connecteur). | session |
session.invited | Invitation envoyée Le SMS ou l'e-mail a été accepté par l'opérateur d'envoi. | session |
session.invitation_failed | Invitation en échec L'envoi du SMS ou de l'e-mail a échoué. La session reste ouverte et l'invitation peut être renvoyée. | session |
session.opened | Lien ouvert Le client a ouvert le lien. | session |
session.live | Vidéo établie La vidéo du client est établie. | session |
photo.created | Photo prise | photo |
video.ready | Vidéo disponible Un clip est terminé et son fichier est téléchargeable. | video |
document.signed | Document signé | document |
measurement.created | Mesure prise | measurement |
session.ended | Session terminée | session |
report.ready | Rapport disponible Le rapport de la session est disponible, résumé compris quand la transcription est active. Il suit session.ended, parfois de quelques minutes. | report |
case.submitted | Dossier déposé par un agent vocal Un agent vocal, le vôtre ou celui de Vizalia, a déposé son dossier pendant l'appel (outil MCP submit_case) : résumé, données, photos et vidéos. Seul événement qui transporte du texte libre. Prend le relais du webhook de dossier réglé aujourd'hui au back-office, et se vérifie comme les autres événements. | case |
L’enveloppe
Section intitulée « L’enveloppe »Chaque envoi a la même forme : type, timestamp, session_id et data, la ressource entière dans son état au moment de l’événement. Le champ object de data dit laquelle. Pour un événement de session, data est la session complète, join_url comprise ; advisor_url n’y est jamais.
{ "id": "evt_01JC41QX7M", "object": "event", "type": "session.invited", "timestamp": "2026-11-04T09:12:02Z", "api_version": "v1", "livemode": true, "session_id": "ses_01JC3W8Z5T9QK2N4R6V8X0Y1Z2", "data": { "id": "ses_01JC3W8Z5T9QK2N4R6V8X0Y1Z2", "object": "session", "legacy_id": 412907, "status": "invited", "livemode": true, "channel": "sms", "to_masked": "+33 6 •• •• •• 78", "reopened": false, "reference": "TICKET-48213", "metadata": { "zendesk_ticket_id": "48213" }, "brand": { "id": "brd_42", "name": "Hospidom" }, "advisor": { "email": "claire.martin@exemple.fr", "first_name": "Claire", "last_name": "Martin" }, "ai_agent": null, "locale": "fr", "join_url": "https://chat.urgentime.com/9f2c4e1a7b3d5e60", "created_at": "2026-11-04T09:12:01Z", "invited_at": "2026-11-04T09:12:02Z", "opened_at": null, "live_at": null, "ended_at": null, "expires_at": "2026-11-04T15:12:01Z", "duration_seconds": null, "counts": { "photos": 0, "videos": 0, "documents": 0, "measurements": 0 }, "report": { "status": "pending", "url": "https://api.vizalia.ai/v1/sessions/ses_01JC3W8Z5T9QK2N4R6V8X0Y1Z2/report", "pdf_url": "https://api.vizalia.ai/v1/sessions/ses_01JC3W8Z5T9QK2N4R6V8X0Y1Z2/report?format=pdf" } }}timestamp est le moment où l’événement s’est produit : il peut précéder l’envoi. Le seul événement qui transporte du texte libre est case.submitted, le dossier déposé par un agent vocal. Aucune route ne relit un dossier : gardez-le à sa réception ; le journal, GET /events, le garde 30 jours.
Accusé de réception et nouvelles tentatives
Section intitulée « Accusé de réception et nouvelles tentatives »Tout code 2xx reçu en moins de 15 secondes vaut accusé de réception. Sinon, nouvelles tentatives après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h et 24 h, avec un léger aléa.
Un abonnement en échec depuis 3 jours est coupé par Vizalia : il passe en status: disabled, avec disabled_reason: failing_deliveries. Corrigez votre serveur, puis réactivez-le :
curl -X PATCH https://api.vizalia.ai/v1/webhook_endpoints/we_01JC40AB12 \ -H "Authorization: Bearer $VIZALIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": "enabled"}'Quand un envoi échoue
Section intitulée « Quand un envoi échoue »Le champ error du résultat d’un envoi, par exemple celui de POST /webhook_endpoints/{endpoint_id}/test, prend l’un de ces codes stables, nul si l’envoi a réussi :
error | Cause |
|---|---|
http_<statut> | Votre serveur a répondu hors 2xx, par exemple http_500. |
timeout | Pas de réponse dans les 15 secondes. |
tls_error | La connexion TLS a échoué, par exemple sur un certificat invalide. |
dns_error | Le nom d'hôte ne se résout pas. |
connection_error | La connexion a été refusée ou interrompue. |
unreachable | L'hôte est injoignable. |
address_not_allowed | L'adresse résout vers un réseau privé ou réservé : Vizalia n'y envoie rien. |
secret_unavailable | Le secret de signature n'a pas pu être lu côté Vizalia ; écrivez au support s'il persiste. |
Rattraper un envoi manqué
Section intitulée « Rattraper un envoi manqué »GET /events garde les événements des 30 derniers jours, tels qu’ils ont été envoyés, les plus récents d’abord. Il sert aussi à suivre une session sans webhook. Filtrez par type, session_id ou created_after.
curl "https://api.vizalia.ai/v1/events?type=report.ready&created_after=2026-11-04T00:00:00Z" \ -H "Authorization: Bearer $VIZALIA_API_KEY"created_after est inclusif : un événement daté de cette heure exacte est renvoyé. Pour rattraper régulièrement, relisez le journal depuis le dernier événement traité, avec une petite marge, et écartez les événements déjà vus par leur id, le même que celui du webhook :
// Rattrapage par le journal (Node.js 18 ou plus) : relit les événements depuis// le dernier traité, au cas où un webhook se serait perdu. Le journal garde// 30 jours d'événements, les plus récents d'abord ; created_after est inclusif.const API = process.env.VIZALIA_API_BASE ?? 'https://api.vizalia.ai/v1';const CLE = process.env.VIZALIA_API_KEY;
export async function rattraper(depuis, traiter, dejaVu) { const evenements = []; let apres; do { const parametres = new URLSearchParams({ created_after: depuis, limit: '100', ...(apres ? { starting_after: apres } : {}) }); const reponse = await fetch(`${API}/events?${parametres}`, { headers: { Authorization: `Bearer ${CLE}` } }); if (!reponse.ok) throw new Error(`GET /events : ${reponse.status}`); const page = await reponse.json(); evenements.push(...page.data); apres = page.has_more ? page.data.at(-1).id : undefined; } while (apres);
// Du plus ancien au plus récent, et chaque événement une seule fois : le même // id arrive par webhook et par le journal. for (const evenement of evenements.reverse()) { if (await dejaVu(evenement.id)) continue; await traiter(evenement); } return evenements.at(-1)?.timestamp ?? depuis;}
// Par exemple toutes les 5 minutes, depuis le dernier horodatage traité moins une marge :// dernier = await rattraper(new Date(Date.parse(dernier) - 120_000).toISOString(), traiter, dejaVu);Aucune route de l’API ne liste les envois d’un abonnement ni ne rejoue un envoi : le journal est le moyen de rattraper. Dans le back-office, l’onglet Webhooks montre les livraisons de chaque abonnement et rejoue un envoi.
Développer en local
Section intitulée « Développer en local »Un abonnement exige une adresse https publique : localhost est refusé. Sur un poste de développeur, exposez votre récepteur par un tunnel HTTPS (cloudflared, ngrok…), puis abonnez son adresse avec une clé de test : l’abonnement reçoit les événements des sessions de test, que simulate fait avancer sans téléphone. Sans tunnel, suivez les sessions par le journal, GET /events.
Renouveler le secret
Section intitulée « Renouveler le secret »POST /webhook_endpoints/{endpoint_id}/rotate_secret crée un nouveau secret, renvoyé une seule fois. Pendant 24 heures, chaque envoi porte deux signatures, l’une avec l’ancien secret et l’autre avec le nouveau : déployez le nouveau secret pendant ce délai, la vérification passe avec l’un comme avec l’autre.
