WebSockets
L'API Voki utilise Phoenix Channels pour la communication en temps réel. Tous les canaux nécessitent une authentification via token JWT.
Connexion
URL de Connexion
wss://voki.avanter.com.br/socket/websocket?token={jwt_access_token}Exemple avec JavaScript
import { Socket } from "phoenix"
const socket = new Socket("wss://voki.avanter.com.br/socket/websocket", {
params: { token: accessToken }
})
socket.connect()
// Rejoindre un canal
const channel = socket.channel("queue:department_id", {})
channel.join()
.receive("ok", (resp) => console.log("Connecté :", resp))
.receive("error", (resp) => console.log("Erreur :", resp))Canaux Disponibles
| Canal | Description | Événements |
|---|---|---|
call:{call_id} | Signalisation d'appel | signal, ice_candidate, join, leave |
queue:{department_id} | File d'attente | customer_joined, customer_left, call_assigned |
presence:{department_id} | Présence des agents | presence_state, presence_diff |
notification:{user_id} | Notifications personnelles | new_notification, appointment_reminder |
Canal : call:
Canal pour la signalisation WebRTC et le contrôle de l'appel vidéo.
Événements Envoyés par le Client
signal
Envoie une offre/réponse SDP pour la signalisation WebRTC.
channel.push("signal", {
type: "offer", // "offer" | "answer"
sdp: "v=0\r\no=- ..."
})ice_candidate
Envoie un candidat ICE pour la négociation de connectivité.
channel.push("ice_candidate", {
candidate: "candidate:1234567890 1 udp ...",
sdpMid: "0",
sdpMLineIndex: 0
})mute
Active/désactive le mode muet audio/vidéo.
channel.push("mute", { type: "audio", muted: true })
channel.push("mute", { type: "video", muted: true })Événements Reçus par le Client
signal
Reçoit une offre/réponse SDP du serveur SFU.
channel.on("signal", (payload) => {
// payload: { type: "answer", sdp: "v=0\r\n..." }
peerConnection.setRemoteDescription(new RTCSessionDescription(payload))
})ice_candidate
Reçoit un candidat ICE du serveur.
channel.on("ice_candidate", (payload) => {
peerConnection.addIceCandidate(new RTCIceCandidate(payload))
})participant_joined
Notifie qu'un participant a rejoint l'appel.
channel.on("participant_joined", (payload) => {
// payload: { user_id: "...", name: "João", role: "attendant" }
})participant_left
Notifie qu'un participant a quitté l'appel.
channel.on("participant_left", (payload) => {
// payload: { user_id: "..." }
})call_ended
Notifie que l'appel a été terminé.
channel.on("call_ended", (payload) => {
// payload: { reason: "completed" }
})monitor_start / whisper_start / barge_start
Événements de supervision (reçus par l'agent lorsqu'un superviseur surveille, chuchote ou intervient).
channel.on("whisper_start", (payload) => {
// payload: { supervisor_id: "...", supervisor_name: "Maria" }
// L'agent peut entendre le superviseur, mais pas le client
})Canal : queue:
Canal pour surveiller la file d'attente d'un département en temps réel.
Événements Reçus
customer_joined
Un client a rejoint la file d'attente.
channel.on("customer_joined", (payload) => {
// payload: {
// call_id: "...",
// customer_name: "Carlos",
// position: 3,
// queue_size: 5
// }
})customer_left
Un client a quitté la file d'attente (abandon ou prise en charge).
channel.on("customer_left", (payload) => {
// payload: { call_id: "...", reason: "assigned" | "timeout" | "cancelled" }
})call_assigned
Un appel a été attribué à un agent.
channel.on("call_assigned", (payload) => {
// payload: { call_id: "...", user_id: "...", user_name: "João" }
})queue_update
Mise à jour générale de l'état de la file d'attente.
channel.on("queue_update", (payload) => {
// payload: { queue_size: 4, avg_wait_time: 25, online_agents: 3 }
})Canal : presence:
Canal pour suivre la présence en ligne des agents d'un département. Utilise Phoenix Presence pour la synchronisation distribuée.
Événements Reçus
presence_state
État complet de présence lors de l'entrée dans le canal.
import { Presence } from "phoenix"
const presence = new Presence(channel)
presence.onSync(() => {
const users = presence.list((id, { metas }) => ({
id,
name: metas[0].name,
status: metas[0].status, // "online" | "busy" | "away"
current_call_id: metas[0].current_call_id
}))
console.log("Agents en ligne :", users)
})presence_diff
Différences incrémentales (arrivées/départs).
presence.onJoin((id, current, newPres) => {
console.log(`${newPres.metas[0].name} est en ligne`)
})
presence.onLeave((id, current, leftPres) => {
console.log(`${leftPres.metas[0].name} est hors ligne`)
})Canal : notification:
Canal pour les notifications personnelles de l'utilisateur.
Événements Reçus
new_notification
Nouvelle notification.
channel.on("new_notification", (payload) => {
// payload: {
// type: "call_assigned" | "appointment_reminder" | "system",
// title: "Nouvel appel attribué",
// body: "Carlos Ferreira attend au Support Technique",
// data: { call_id: "..." }
// }
})appointment_reminder
Rappel de rendez-vous.
channel.on("appointment_reminder", (payload) => {
// payload: {
// appointment_id: "...",
// title: "Consultation de suivi",
// scheduled_at: "2026-02-20T10:00:00Z",
// customer_name: "Carlos Ferreira",
// minutes_until: 15
// }
})Gestion des Erreurs
Reconnexion Automatique
Le socket Phoenix effectue une reconnexion automatique avec backoff exponentiel :
const socket = new Socket(url, {
params: { token: accessToken },
reconnectAfterMs: (tries) => [1000, 2000, 5000, 10000][tries - 1] || 10000
})Token Expiré
Lorsque le token JWT expire, le socket sera déconnecté. Renouvelez le token et reconnectez-vous :
socket.onClose(() => {
// Vérifier si le token a expiré
if (isTokenExpired(accessToken)) {
refreshToken().then((newToken) => {
socket.params.token = newToken
socket.connect()
})
}
})Identifiants TURN
Pour contourner les restrictions NAT dans les appels WebRTC, obtenez des identifiants TURN :
GET /api/v1/turn/credentialsExemple de Requête
curl -X GET https://voki.avanter.com.br/api/v1/turn/credentials \
-H "Authorization: Bearer eyJhbGci..." \
-H "X-Tenant: avanter"Réponse de Succès (200)
{
"data": {
"urls": [
"turn:voki.avanter.com.br:3478?transport=udp",
"turn:voki.avanter.com.br:3478?transport=tcp"
],
"username": "1708300800:user123",
"credential": "abcdef1234567890",
"ttl": 86400
}
}Note
Les identifiants TURN sont temporaires (TTL de 24h) et doivent être renouvelés périodiquement. L'endpoint public /api/v1/call/turn-credentials est également disponible pour les clients sans authentification JWT.
