aethexDocs

Appels WebRTC

Voix en temps réel entre le navigateur d'un utilisateur (ou une app mobile) et un agent Aethex. Aucun numéro de téléphone requis ; juste une poignée de main HTTP et un chemin audio direct. La latence reste dans la même plage qu'un appel téléphonique.

Pourquoi WebRTC

Pour toute expérience in-app, dans le navigateur ou mobile, WebRTC est le bon transport pour la voix en direct. Trois raisons :

  • API native du navigateur. RTCPeerConnection gère déjà la capture du micro, l'encodage, la mise en tampon de gigue, la récupération de perte de paquets et l'annulation d'écho. Vous n'avez pas besoin d'une bibliothèque audio personnalisée.
  • Traversée NAT. ICE gère les pare-feux d'entreprise, les réseaux d'hôtels et les routeurs domestiques. Les appels se connectent depuis des endroits où un WebSocket brut ne peut pas.
  • Débit adaptatif. Opus suit la qualité réseau en temps réel et se dégrade gracieusement. Les utilisateurs sur un mauvais WiFi peuvent encore avoir une conversation ; ils ne coupent pas.

Comment fonctionne un appel

Trois acteurs. La configuration passe par votre serveur pour que votre clé API reste en place ; une fois la poignée de main terminée, l'audio circule directement entre le navigateur et Aethex.

NAVIGATEURmicro + audioVOTRE SERVEURvotre backendAETHEXagent vocaldémarrer l'appelsession + ICEPOST /connectSDP answer + ICEWebRTC audio · DTLS-SRTPmédia direct, sans passer par votre serveur
  1. Le navigateur demande à votre serveur de démarrer un appel.
  2. Votre serveur appelle POST/conversation/connect avec votre clé API.
  3. Aethex retourne un session_id et un ice_config.
  4. Votre serveur les transmet au navigateur.
  5. Le navigateur construit une offre SDP, la poste via votre serveur vers POST/conversation/{session_id}/offer ; votre serveur retourne la réponse SDP. Le navigateur la définit et l'audio commence à circuler.

Après l'étape 5, votre serveur est complètement hors du chemin audio. Il ne rentre dans la boucle que pour retourner un /tool-result quand l'agent invoque un outil, ou pour /end l'appel.

Où ouvrir la session

Par défaut, les sessions sont ouvertes depuis votre serveur, qui gère également la signalisation SDP — le navigateur envoie son offre à votre serveur, qui la transmet à Aethex avec votre clé API et retourne la réponse SDP. Une fois le handshake terminé, l'audio circule directement entre le navigateur et Aethex. La seule exception est le flux de jeton d'appel éphémère (ci-dessous) : le navigateur communique directement avec Aethex à l'aide d'un jeton de courte durée, et votre serveur se contente de générer le jeton.

Depuis votre serveur
POST/conversation/connect

Appelez le point d'accès avec votre clé API, puis transmettez le session_id et l'ice_config retournés au navigateur.

Astuce
N'intégrez jamais une clé API brute dans du JavaScript côté navigateur. Quiconque ouvre les outils de développement la verra. Faites appeler /conversation/connect par votre serveur et transmettez les détails de la session au navigateur.

Sans proxy : jetons d'appel éphémères

Si vous préférez ne pas relayer chaque requête de signalisation, générez un jeton d'appel de courte durée sur votre serveur et transmettez-le au navigateur. Le navigateur communique alors directement avec Aethex — le jeton remplace votre clé API sur /connect, /offer, /ice, /status et /end. Votre seul code côté serveur est une route de génération d'une ligne, et la clé ae_live_ n'atteint jamais le navigateur.

NAVIGATEURmicro + audioVOTRE SERVEURgénérer le jeton · un appelAETHEXagent vocaljetonPOST /tokenjetonjeton Bearer → /connect · /offer · /iceWebRTC audio · DTLS-SRTP
  1. Votre serveur appelle POST /conversation/token avec votre clé API et un agent_id, puis renvoie le jeton au navigateur.
  2. Le navigateur appelle lui-même /connect, /offer, /ice, /status et /end en envoyant l'en-tête Authorization: Bearer <token> — sans aller-retour par requête via votre serveur.
  3. Le jeton est limité à ce seul agent et vit juste assez longtemps pour l'appel — connexion, ICE en trickle, interrogation de /status et raccrochage avec /end (sa durée de vie correspond par défaut à la durée maximale d'appel de l'agent plus une courte marge). Éphémère et limité à un seul agent, donc une fuite reste contenue ; générez-en un nouveau par appel.
Astuce
Un jeton d'appel ne fonctionne que pour l'unique agent pour lequel il a été généré, et uniquement pour la signalisation de conversation de cet agent (/connect, /offer, /ice, /status, /end) ; rien d'autre dans l'API. /tool-result est la seule exception : il n'est pas authentifié par jeton, donc les agents à outils côté client continuent de le relayer via votre serveur. Il ne porte que les portées calls de la clé qui l'a généré : c'est l'équivalent sûr pour le navigateur de votre clé API côté serveur.
Astuce
Fonctionne depuis n'importe quelle origine. Le navigateur s'authentifie avec le jeton (un identifiant bearer, pas un cookie), donc Aethex accepte les appels de signalisation directs depuis n'importe quel site, sans liste d'autorisation d'origines à configurer. Les requêtes que vous relayez via votre propre serveur sont de même origine et, comme toujours, non concernées.

Exemple complet

La configuration minimale fonctionnelle : votre serveur génère une session et gère la signalisation SDP, votre navigateur construit l'offre et lit l'audio de réponse.

server.py
python
import os, requests
from flask import Flask, jsonify, request as req

app = Flask(__name__)

@app.post("/api/start-call")
def start_call():
    """Browser POSTs here; we mint a session for an agent."""
    resp = requests.post(
        "https://api.aethexai.com/api/v1/conversation/connect",
        headers={"X-API-Key": os.environ["AETHEX_API_KEY"]},
        json={"agent_id": "8a1b2c3d-4e5f-6789-abcd-ef0123456789"},
    )
    return jsonify(resp.json()), resp.status_code  # session_id + ice_config

@app.post("/api/send-offer")
def send_offer():
    """Browser POSTs its SDP offer here; we forward it to Aethex with the API key."""
    data = req.json
    resp = requests.post(
        f"https://api.aethexai.com/api/v1/conversation/{data['session_id']}/offer",
        headers={"X-API-Key": os.environ["AETHEX_API_KEY"]},
        json={"sdp": data["sdp"], "type": data["type"]},
    )
    return jsonify(resp.json()), resp.status_code  # sdp (answer) + type + pc_id

Le côté navigateur se connecte avec l'ID de session et la configuration ICE retournés par le serveur :

client.ts
typescript
async function startCall() {
  // 1. Get session details from your server
  const session = await fetch("/api/start-call", { method: "POST" }).then(r => r.json())

  // 2. Build the peer connection with Aethex's ICE config
  const pc = new RTCPeerConnection(session.ice_config)
  const appChannel = pc.createDataChannel("aethex-app", { ordered: true })
  let pingTimer: ReturnType<typeof setInterval> | null = null
  const stopPing = () => {
    if (pingTimer) {
      clearInterval(pingTimer)
      pingTimer = null
    }
  }
  appChannel.onopen = () => {
    const sendPing = () => {
      if (appChannel.readyState === "open") appChannel.send(`ping:${Date.now()}`)
    }
    sendPing()
    pingTimer = setInterval(sendPing, 1000)
  }
  appChannel.onclose = stopPing
  appChannel.onerror = stopPing
  pc.ontrack = (e) => (document.querySelector("audio")!.srcObject = e.streams[0])

  // 3. Add the user's mic
  const mic = await navigator.mediaDevices.getUserMedia({ audio: true })
  mic.getTracks().forEach((t) => pc.addTrack(t, mic))

  // 4. Build the offer and wait for ICE gathering to complete before sending.
  //    All candidates land in pc.localDescription.sdp so Aethex can open the
  //    media path immediately — no trickle handshake needed for this demo.
  const offer = await pc.createOffer()
  await pc.setLocalDescription(offer)
  await new Promise(resolve => {
    if (pc.iceGatheringState === "complete") return resolve()
    pc.onicegatheringstatechange = () => {
      if (pc.iceGatheringState === "complete") resolve()
    }
  })

  // 5. Exchange SDP through your server proxy (it calls Aethex with your API key)
  const answer = await fetch(
    "/api/send-offer",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: session.session_id, sdp: pc.localDescription.sdp, type: pc.localDescription.type }),
    },
  ).then(r => r.json())
  await pc.setRemoteDescription(answer)

  // Audio is flowing. The agent speaks its first message; the user can interrupt.
}

Piloter l'appel en cours de conversation

Une fois l'audio établi, deux points d'accès permettent à votre serveur de reprendre la boucle :

  • POST/conversation/{session_id}/tool-result — retourner un résultat d'outil côté client à l'agent pour qu'il puisse continuer. Consultez Outils pour le contrat requête/réponse.
  • POST/conversation/{session_id}/end — raccrocher. Draine l'audio en cours et exécute les hooks post-appel avant de fermer.

Après la fin de l'appel

La transcription et l'enregistrement (quand activés sur l'agent) se stabilisent en quelques secondes. Récupérez-les avec GET/conversations/{id}/transcript et GET/recordings/{id}/audio. Le session_id retourné par POST/conversation/connect fait office d'ID de conversation et d'ID d'appel, donc un seul UUID corrèle tout. Consultez Enregistrement & transcription pour ce qui est capturé.

Capacité et facturation

Chaque session WebRTC compte dans la capacité d'appels actifs de votre compte, au même titre qu'un appel téléphonique. Si vous atteignez la limite, POST/conversation/connect retourne 429 avec un Retry-After en-tête ; respectez-le et faites une pause. Les appels WebRTC sont facturés à la minute de temps actif de l'agent, au même tarif que les appels téléphoniques ; consultez Facturation.

Suivant

Pour la forme complète des requêtes et réponses sur chaque point d'accès, consultez Conversation (WebRTC) dans la référence API.