Documentation développeur
Installez, configurez et intégrez Phonevoice — API REST, webhooks signés, widget d'appel embarquable et service Relais.
Démarrage
L'API Phonevoice est une API REST JSON. Vous pouvez lancer et contrôler des appels, acheter et configurer des numéros relais, et gérer des webhooks. Tous les endpoints vivent sous la même URL de base :
https://api.phonevoice.ai/api/v1
Chaque requête doit envoyer votre token en en-tête Bearer et (pour les écritures) du JSON :
Authorization: Bearer YOUR_TOKEN Content-Type: application/json
Vérifiez votre token avec un appel rapide à /whoami :
curl https://api.phonevoice.ai/api/whoami \
-H "Authorization: Bearer YOUR_TOKEN"
const res = await fetch("https://api.phonevoice.ai/api/whoami", {
headers: { Authorization: "Bearer YOUR_TOKEN" }
});
console.log(await res.json());
import requests
r = requests.get(
"https://api.phonevoice.ai/api/whoami",
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
print(r.json())
require "net/http"
require "json"
uri = URI("https://api.phonevoice.ai/api/whoami")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer YOUR_TOKEN"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)
# => { "status": "ok", "id": 42, "name": "…", "email": "…" }
Modes d'intégration
Deux façons d'intégrer Phonevoice à votre produit — et vous pouvez les combiner appel par appel. Intégrez notre interface en iframe pour aller au plus vite, ou pilotez l'API REST + les webhooks quand vous voulez maîtriser le design et garder la donnée chez vous.
Iframe — interface hébergée
Insérez une iframe Phonevoice signée dans votre page. POST /calls/:id/embed_session renvoie un softphone en direct (rôles agent / talk / listen) ; POST /calls/:id/player_session renvoie un lecteur d'enregistrement avec la forme d'onde et les bulles de transcription synchronisées. Phonevoice possède le design et gère l'audio — aucun code média de votre côté.
Natif — votre propre interface (API + webhooks)
Pilotez tout depuis l'API REST et recevez des webhooks signés. Chaque événement phone_call.completed contient la transcription, le résumé d'analyse IA et le recording_url : vous affichez vos propres écrans et gardez la donnée chez vous. Diffusez la transcription en direct via WebSocket pendant que l'appel est encore en cours.
| Ce que vous obtenez | Iframe | Natif (API + webhooks) |
|---|---|---|
| Qui possède le design | Phonevoice | Vous |
| Effort d'intégration | Coller une iframe | Gérer des webhooks |
| Lecture audio | Intégrée | Via recording_url |
| Où vivent les données d'appel | Hébergées par Phonevoice | Chez vous |
Combinez librement, appel par appel : un CRM peut intégrer le lecteur quand un enregistrement existe et basculer sur ses propres bulles de transcription (construites depuis la transcription du webhook) sinon — même appel, les deux modes.
Authentification
Authentifiez-vous avec un token Bearer. Il en existe deux types — utilisez un token de projet pour les intégrations serveur à serveur :
Token API de projet (recommandé)
Limité à un seul projet — ses agents, numéros et webhooks sont isolés de vos autres projets. Trouvez-le dans le tableau de bord, sous votre projet → « API Configuration ». Traitez-le comme un mot de passe : côté serveur uniquement, jamais dans du code client.
Token utilisateur
Émis pour un compte utilisateur par le parcours d'inscription (POST /api/signup → POST /api/confirm renvoie un user_token). Utile pour l'app mobile et l'accès personnel.
Un token peut être passé dans l'en-tête Authorization (préféré) ou en paramètre de requête ?api_token= / ?user_token=.
Erreurs & codes de statut
Les erreurs renvoient un corps JSON avec un message « error » et un statut HTTP correspondant.
| Code | Meaning |
|---|---|
| 200 / 201 | OK / created |
| 401 | Token manquant ou invalide |
| 403 | Authentifié mais vous ne possédez pas cette ressource |
| 404 | Introuvable |
| 422 | Erreur de validation (voir « error ») |
API Appels
Lancez un appel sortant avec un agent IA (ou un bridge entre deux humains), puis contrôlez-le.
| POST | /api/v1/calls/initiate | Démarrer un appel sortant |
| GET | /api/v1/agents | Lister vos agents (utilisez un id comme agent_id dans /calls/initiate) |
| POST | /api/v1/calls/:id/hangup | Raccrocher un appel |
| POST | /api/v1/calls/:id/add_monitor | Faire entrer un superviseur, micro coupé (écoute en direct) |
| POST | /api/v1/calls/:id/embed_session | URL d'iframe signée pour le softphone navigateur / l'écoute |
| POST | /api/v1/calls/:id/player_session | URL d'iframe signée pour rejouer l'enregistrement + la transcription |
| GET | /api/v1/calls/:id/costs | Détail des coûts & prix (EUR) d'un appel |
Exemple — démarrer un appel avec l'un de vos agents :
curl -X POST https://api.phonevoice.ai/api/v1/calls/initiate \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": 1533,
"recipient_phone_number": "+33612345678",
"inbound_phone_number": "+33159133354",
"enable_recording": true,
"enable_transcription": true,
"enable_analysis": true
}'
const res = await fetch("https://api.phonevoice.ai/api/v1/calls/initiate", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
agent_id: 1533,
recipient_phone_number: "+33612345678",
inbound_phone_number: "+33159133354",
enable_recording: true,
enable_transcription: true,
enable_analysis: true,
}),
});
const call = await res.json();
console.log(call.phone_call_id);
import requests
res = requests.post(
"https://api.phonevoice.ai/api/v1/calls/initiate",
headers={"Authorization": "Bearer YOUR_TOKEN"},
json={
"agent_id": 1533,
"recipient_phone_number": "+33612345678",
"inbound_phone_number": "+33159133354",
"enable_recording": True,
"enable_transcription": True,
"enable_analysis": True,
},
)
print(res.json()["phone_call_id"])
require "net/http"
require "json"
uri = URI("https://api.phonevoice.ai/api/v1/calls/initiate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_TOKEN"
req["Content-Type"] = "application/json"
req.body = {
agent_id: 1533,
recipient_phone_number: "+33612345678",
inbound_phone_number: "+33159133354",
enable_recording: true,
enable_transcription: true,
enable_analysis: true,
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["phone_call_id"]
# => 201 { "status":"success", "phone_call_id": 90210, "telephony_ref":"CA…", "cable_token":"…" }
Configurez la destination des résultats avec les Webhooks (ci-dessous) — les webhooks se gèrent séparément, pas appel par appel. cable_token permet de diffuser la transcription en direct (voir Transcription live).
Appel Direct (Bridge) & softphone navigateur
Faites parler un agent humain (« vendeur ») directement avec un client — enregistré, transcrit et résumé comme un appel relais, mais à l'initiative de l'agent. Deux modes, tous deux via /calls/initiate avec call_type=bridge :
🎧 Softphone navigateur
L'agent parle depuis le navigateur — aucune ligne téléphonique. Envoyez vendeur_mode:"browser", puis intégrez l'iframe softphone signée. Le client est appelé dès que l'agent se connecte.
📞 Bridge téléphone
Phonevoice appelle d'abord la ligne de l'agent (inbound_phone_number), puis met le client en relation. Sans navigateur.
1 — Lancez l'appel (mode navigateur ici ; pour un bridge téléphone, omettez vendeur_mode et passez inbound_phone_number = la ligne de l'agent) :
curl -X POST https://api.phonevoice.ai/api/v1/calls/initiate \
-H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{ "call_type":"bridge", "vendeur_mode":"browser",
"recipient_phone_number":"+33612345678" }'
# => 201 { "phone_call_id": 90210, "cable_token": "…" }
const API = "https://api.phonevoice.ai/api/v1";
const headers = { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "application/json" };
// Start a bridge call (browser mode)
const { phone_call_id, cable_token } = await fetch(`${API}/calls/initiate`, {
method: "POST", headers,
body: JSON.stringify({ call_type: "bridge", vendeur_mode: "browser", recipient_phone_number: "+33612345678" }),
}).then(r => r.json());
import requests
API = "https://api.phonevoice.ai/api/v1"
headers = {"Authorization": "Bearer YOUR_TOKEN"}
# Start a bridge call (browser mode)
call = requests.post(f"{API}/calls/initiate", headers=headers, json={
"call_type": "bridge", "vendeur_mode": "browser",
"recipient_phone_number": "+33612345678",
}).json()
# call["phone_call_id"], call["cable_token"]
require "faraday"
require "json"
conn = Faraday.new("https://api.phonevoice.ai/api/v1") do |f|
f.headers["Authorization"] = "Bearer YOUR_TOKEN"
f.headers["Content-Type"] = "application/json"
end
call = JSON.parse(conn.post("calls/initiate",
{ call_type: "bridge", vendeur_mode: "browser", recipient_phone_number: "+33612345678" }.to_json).body)
# call["phone_call_id"], call["cable_token"]
2 — Mode navigateur : récupérez une iframe softphone signée et intégrez-la (rôle agent = le vendeur qui parle).
curl -X POST https://api.phonevoice.ai/api/v1/calls/90210/embed_session \
-H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{ "role":"agent" }'
# => { "embed_url":"https://api.phonevoice.ai/embed?t=…", "role":"agent", "expires_in":300 }
const { embed_url } = await fetch("https://api.phonevoice.ai/api/v1/calls/90210/embed_session", {
method: "POST",
headers: { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "application/json" },
body: JSON.stringify({ role: "agent" }),
}).then(r => r.json());
const iframe = Object.assign(document.createElement("iframe"), { src: embed_url, allow: "microphone; autoplay" });
document.body.appendChild(iframe);
import requests
session = requests.post("https://api.phonevoice.ai/api/v1/calls/90210/embed_session",
headers={"Authorization": "Bearer YOUR_TOKEN"}, json={"role": "agent"}).json()
print(session["embed_url"]) # hand this URL to a browser to embed
require "faraday"
require "json"
res = Faraday.post("https://api.phonevoice.ai/api/v1/calls/90210/embed_session",
{ role: "agent" }.to_json,
{ "Authorization" => "Bearer YOUR_TOKEN", "Content-Type" => "application/json" })
puts JSON.parse(res.body)["embed_url"] # hand this URL to a browser to embed
<iframe src="EMBED_URL" allow="microphone; autoplay"></iframe>
| role | Usage |
|---|---|
| agent | Le softphone du vendeur — micro ouvert ; démarre, enregistre et termine l'appel ; appelle le client. |
| talk | Micro ouvert, participant supplémentaire — ne contrôle pas l'appel. |
| listen | Écoute en direct, micro coupé, pour un superviseur. |
Écoute en direct. Un superviseur peut écouter un appel en cours de deux façons : en silence dans le navigateur avec un embed_session de rôle listen (micro coupé), ou depuis un téléphone — POST /calls/:id/add_monitor avec supervisor_phone le fait entrer dans l'appel en direct, micro coupé.
Widget d'appel
Une ligne de JavaScript transforme chaque numéro de téléphone de votre site en bouton d'appel Phonevoice. Le widget décore chaque lien tel: (un MutationObserver suit les SPA et les CRM), monte des boutons explicites là où vous les placez, et ouvre une modale d'appel autonome — appel navigateur, mise en relation « on vous appelle » ou votre agent IA — avec statut et durée en direct, plus la transcription, le résumé IA et le prix quand votre clé les active.
Démarrage rapide — créez une clé dans le tableau de bord, autorisez les domaines de votre site, puis collez une ligne avant la fermeture du body :
<script src="https://phonevoice.ai/widget.js"
data-key="pk_live_YOUR_KEY" defer></script>
Modes, fonctionnalités et limites vivent côté serveur sur la clé : modifiez-les dans le tableau de bord et chaque page qui embarque le widget suit instantanément — rien à redéployer. Gérer vos clés de widget → Essayer la démo en direct →
| Attribut du script | Signification |
|---|---|
| data-key | Obligatoire. Votre clé publiable (pk_live_…) — sûre dans une page publique, voir Sécurité ci-dessous. |
| data-lang | « en » ou « fr ». Par défaut : la langue de la page (puis du navigateur). |
| data-modes | Liste séparée par des virgules pour restreindre davantage les modes côté client, ex. « browser,bridge ». La clé reste la source de vérité. |
| data-tel-links | Mettez « off » pour désactiver la détection automatique des liens tel: (boutons explicites uniquement). |
| data-api | Surcharge de l'origine API — pour tester contre un backend de staging. |
Boutons explicites (sans lien tel:)
Partout où il n'y a pas de lien tel: — fiches CRM, listes de leads — posez un élément placeholder et le widget y monte un bouton d'appel :
<span data-phonevoice-call-button
data-pv-phone="+33159133354"
data-pv-name="Jean Dupont"></span>
| data-* | Signification |
|---|---|
| data-pv-phone | Le numéro à appeler (E.164 de préférence). |
| data-pv-name | Nom du contact affiché dans la modale et attaché à l'appel. |
| data-pv-contacted | Mettez « false » pour masquer le numéro sur le bouton (affiche « Numéro masqué »). |
| data-pv-type / data-pv-id | Métadonnées libres sur votre contact, renvoyées dans l'événement DOM phonevoice:call-requested. |
API JS : chaque clic émet un événement bouillonnant phonevoice:call-requested (detail : type, id, name, phone) ; window.PhoneVoiceWidget.mount() rescanne la page (les navigations Turbo sont gérées automatiquement) et window.PhoneVoiceWidget.open(phone, name) ouvre la modale d'appel par programme.
Clés publiables & sécurité
Une clé pk_live est conçue pour être embarquée dans des pages publiques. Contrairement à un token Bearer, elle ne peut rien lire — ni appels, ni transcriptions, ni soldes — elle ne peut que créer des sessions d'appel qui satisfont toujours chaque garde-fou défini sur la clé :
Liste de domaines autorisés
Les sessions ne sont créées que si l'Origin/Referer de la requête correspond à votre liste (*.exemple.com accepté). Une clé sans domaine est inerte.
Pays de destination
Anti-fraude : la plateforme refuse de composer tout numéro hors de votre liste de pays (par défaut : votre propre pays). En mode mise en relation, le champ « numéro du visiteur » est désactivé par défaut.
Plafonds & limites
Limite par IP visiteur (5 appels/heure par défaut) et plafond de dépense quotidien (50 € par défaut) — une fois atteint, le widget cesse de passer des appels jusqu'au lendemain.
Modes & révocation
Seuls les modes que vous cochez (navigateur / mise en relation / agent IA) sont proposés. Révoquez ou supprimez la clé à tout moment — les widgets embarqués cessent immédiatement de proposer l'appel.
Tarification
Les appels du widget sont des appels Phonevoice ordinaires, facturés au propriétaire de la clé aux tarifs standard : appels navigateur et « on vous appelle » au tarif Click-to-call (bridge), appels d'agent IA comme tout appel d'agent. Le visiteur ne voit le prix que si « Afficher le prix » est activé sur la clé.
Voir les tarifs à la minute par pays →
Sous le capot, le widget parle à trois endpoints publics — authentifiés par la clé publiable et l'Origin de la page, jamais par un secret :
| GET | /api/widget/config | La configuration publique de la clé (modes, fonctionnalités, objectif prérempli) |
| POST | /api/widget/sessions | Créer une session d'appel (renvoie un token de session de courte durée) |
| GET | /api/widget/calls/:id | Statut, durée, transcription en direct — limité par le token de session |
Service Relais
Un relais transforme un vrai numéro de téléphone en ligne de renvoi. Les appels entrants sont renvoyés vers un numéro personnel pendant que Phonevoice enregistre, transcrit, résume et déclenche des webhooks — sans agent IA ni application. Voir la présentation du Relais pour le contexte produit ; ceci est la recette d'intégration.
1 — Trouver & acheter un numéro
Cherchez des numéros disponibles, puis achetez-en un et définissez vers où il doit renvoyer.
2 — Configurer le renvoi
Changez la destination ou suspendez le relais à tout moment avec PATCH.
3 — Recevoir des événements enrichis
Les appels entrants sonnent sur votre téléphone (identifiant d'appelant = le numéro relais) ; à la fin, un webhook phone_call.completed transporte l'enregistrement, le transcript et le résumé IA.
# 1. Search available US numbers
curl "https://api.phonevoice.ai/api/v1/numbers/available?country=US&type=local&area_code=415" \
-H "Authorization: Bearer YOUR_TOKEN"
# 2. Buy one and forward it to your phone
curl -X POST https://api.phonevoice.ai/api/v1/numbers \
-H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{ "phone_number":"+14155550123", "forward_to":"+33612345678", "country":"US" }'
# 3. Later — change the destination or pause it
curl -X PATCH https://api.phonevoice.ai/api/v1/numbers/123 \
-H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{ "forward_to":"+33700000000", "relay_enabled": true }'
const API = "https://api.phonevoice.ai/api/v1";
const headers = { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "application/json" };
// 1. Search available numbers
const avail = await fetch(`${API}/numbers/available?country=US&type=local&area_code=415`, { headers });
// 2. Buy + forward to your phone
const num = await fetch(`${API}/numbers`, {
method: "POST", headers,
body: JSON.stringify({ phone_number: "+14155550123", forward_to: "+33612345678", country: "US" }),
}).then(r => r.json());
// 3. Update the destination / pause
await fetch(`${API}/numbers/${num.id}`, {
method: "PATCH", headers,
body: JSON.stringify({ forward_to: "+33700000000", relay_enabled: true }),
});
import requests
API = "https://api.phonevoice.ai/api/v1"
headers = {"Authorization": "Bearer YOUR_TOKEN"}
# 1. Search available numbers
requests.get(f"{API}/numbers/available",
params={"country": "US", "type": "local", "area_code": "415"}, headers=headers)
# 2. Buy + forward to your phone
num = requests.post(f"{API}/numbers", headers=headers,
json={"phone_number": "+14155550123", "forward_to": "+33612345678", "country": "US"}).json()
# 3. Update the destination / pause
requests.patch(f"{API}/numbers/{num['id']}", headers=headers,
json={"forward_to": "+33700000000", "relay_enabled": True})
require "faraday"
require "json"
conn = Faraday.new("https://api.phonevoice.ai/api/v1") do |f|
f.headers["Authorization"] = "Bearer YOUR_TOKEN"
f.headers["Content-Type"] = "application/json"
end
# 1. Search available numbers
conn.get("numbers/available", country: "US", type: "local", area_code: "415")
# 2. Buy + forward to your phone
num = JSON.parse(conn.post("numbers",
{ phone_number: "+14155550123", forward_to: "+33612345678", country: "US" }.to_json).body)
# 3. Update the destination / pause
conn.patch("numbers/#{num['id']}",
{ forward_to: "+33700000000", relay_enabled: true }.to_json)
Numéros
| GET | /api/v1/numbers | Lister vos numéros + config de relais |
| GET | /api/v1/numbers/available | Chercher des numéros achetables (country, type, area_code, contains) |
| POST | /api/v1/numbers | Acheter un numéro (définir forward_to pour l'utiliser en relais) |
| PATCH | /api/v1/numbers/:id | Définir forward_to / basculer relay_enabled |
Webhooks
Enregistrez des endpoints pour recevoir les événements d'appel. Les webhooks sont limités à votre projet. Le secret n'est affiché qu'une fois, à la création — conservez-le pour vérifier les signatures.
| GET | /api/v1/webhooks | Lister vos webhooks |
| POST | /api/v1/webhooks | Créer un webhook (renvoie le secret une seule fois) |
| PATCH | /api/v1/webhooks/:id | Mettre à jour url / name / events / active |
| DELETE | /api/v1/webhooks/:id | Supprimer un webhook |
| GET | /api/v1/webhooks/:id/calls | Journal de livraison (50 dernières tentatives) |
curl -X POST https://api.phonevoice.ai/api/v1/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
-d '{ "url":"https://example.com/hooks/phonevoice",
"events":["phone_call.completed","sms.received"], "name":"My CRM" }'
const res = await fetch("https://api.phonevoice.ai/api/v1/webhooks", {
method: "POST",
headers: { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/hooks/phonevoice",
events: ["phone_call.completed", "sms.received"],
name: "My CRM",
}),
});
const { secret } = await res.json(); // store this — shown only once
import requests
res = requests.post(
"https://api.phonevoice.ai/api/v1/webhooks",
headers={"Authorization": "Bearer YOUR_TOKEN"},
json={
"url": "https://example.com/hooks/phonevoice",
"events": ["phone_call.completed", "sms.received"],
"name": "My CRM",
},
)
secret = res.json()["secret"] # store this — shown only once
require "net/http"
require "json"
uri = URI("https://api.phonevoice.ai/api/v1/webhooks")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_TOKEN"
req["Content-Type"] = "application/json"
req.body = {
url: "https://example.com/hooks/phonevoice",
events: ["phone_call.completed", "sms.received"],
name: "My CRM",
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
secret = JSON.parse(res.body)["secret"] # store this — shown only once
# => 201 { "id":35, "url":"…", "events":[…], "secret":"whsec_…" } ← store the secret
Omettez « events » (ou utilisez ["*"]) pour recevoir tous les événements. Chaque livraison est un POST avec cette enveloppe :
{
"id": "evt_… (unique per delivery)",
"type": "phone_call.completed",
"created_at": "2026-06-04T09:23:56Z",
"data": { … } // event-specific (see below)
}
Événements webhook
| type | Se déclenche quand |
|---|---|
| phone_call.started | L'appel se connecte (en cours). |
| phone_call.completed | L'appel se termine — le statut du payload est « completed », ou « stopped » si raccroché via l'API/la console (tout aussi final). Re-déclenché à mesure que le transcript et le résumé IA deviennent disponibles. |
| phone_call.<status> | Autres transitions : failed, no-answer, busy, error… |
| phone_call.transcript | Chaque message de transcription, en temps réel (appels agent IA). |
| sms.received | Un SMS entrant arrive sur un numéro avec relais activé. |
data pour les événements phone_call.* :
{
"id": 90210, "status": "completed", "duration": 50, "cost": 0.12,
"direction": "inbound", "from": "+33783484044", "to": "+33612345678",
"relay_number": "+33159133354",
"recording_url": "https://…/bridge_90210.mp3",
"transcription": "[Caller] Hello…\n[Callee] Hello, …",
"analysis": { "summary": "…", "status": "fulfilled", "interest_level": 7 }
}
data pour sms.received :
{ "direction":"inbound", "from":"+33783484044", "to":"+33159133354",
"relay_number":"+33159133354", "body":"Hello!" }
Vérifier les signatures de webhook
Chaque livraison est signée. L'en-tête Phonevoice-Signature vaut t=<unix_timestamp>,v1=<hex>, où v1 est un HMAC-SHA256 de « {timestamp}.{raw_body} » avec votre secret de webhook comme clé. Recalculez-le et comparez en temps constant.
// Node.js (Express) — verify before trusting the payload
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
# Python (Flask) — verify before trusting the payload
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=") for p in header.split(","))
expected = hmac.new(secret.encode(),
f"{parts['t']}.{raw_body.decode()}".encode(),
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
# Ruby (Rails) — verify before trusting the payload
require "openssl"
def verify(raw_body, header, secret)
parts = header.split(",").map { |p| p.split("=") }.to_h
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{parts['t']}.#{raw_body}")
Rack::Utils.secure_compare(expected, parts["v1"])
end
Transcription live (ActionCable)
Pour un transcript en temps réel pendant l'appel, abonnez-vous au canal de l'appel via WebSocket en utilisant le cable_token renvoyé par /calls/initiate (et émis pour les appels relais). Les messages arrivent au fil de la conversation.
wss://api.phonevoice.ai/cable
→ subscribe { channel: "CallsChannel", id: 90210, token: "<cable_token>" }
← { type: "transcript", role: "user", content: "…" }
← { type: "status_changed", status: "completed" }
// Browser — subscribe to the live transcript over ActionCable
const cable = new WebSocket("wss://api.phonevoice.ai/cable");
const identifier = JSON.stringify({ channel: "CallsChannel", id: 90210, token: "CABLE_TOKEN" });
cable.onopen = () => cable.send(JSON.stringify({ command: "subscribe", identifier }));
cable.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (!msg.message) return; // welcome / ping / confirm_subscription
const m = msg.message;
if (m.type === "transcript") console.log(`${m.role}: ${m.content}`);
if (m.type === "status_changed") console.log("status:", m.status);
};
# pip install websocket-client
import json, websocket
ws = websocket.create_connection("wss://api.phonevoice.ai/cable")
identifier = json.dumps({"channel": "CallsChannel", "id": 90210, "token": "CABLE_TOKEN"})
ws.send(json.dumps({"command": "subscribe", "identifier": identifier}))
while True:
m = json.loads(ws.recv()).get("message")
if not m: # welcome / ping / confirm_subscription
continue
if m["type"] == "transcript":
print(m["role"], ":", m["content"])
elif m["type"] == "status_changed":
print("status:", m["status"])
Vous préférez une UI clé en main ? Appelez /calls/:id/player_session pour obtenir une iframe signée et intégrable avec la forme d'onde + les bulles de transcription synchronisées — aucun traitement audio de votre côté.
Recettes
Des parcours de bout en bout courants, chacun assemblant les endpoints ci-dessus. Choisissez celui qui correspond à votre cas d'usage.
Lancer un appel agent IA et récupérer le résultat
POST /calls/initiate avec un agent_id, puis traitez le webhook phone_call.completed — il contient la transcription, le résumé IA et le recording_url.
Ajouter un bouton click-to-call
Collez le widget d'appel d'une ligne avec une clé publiable — chaque lien tel: devient un bouton d'appel — ou faites le vôtre avec un appel bridge (call_type=bridge) et le softphone intégré.
Intégrer l'enregistrement + la transcription
POST /calls/:id/player_session pour une iframe signée avec la forme d'onde et les bulles de transcription synchronisées — aucun traitement audio de votre côté.
Écouter un appel en direct
Intégrez un embed_session de rôle listen pour un superviseur silencieux dans le navigateur, ou POST /calls/:id/add_monitor avec supervisor_phone pour le faire entrer par téléphone.
Diffuser la transcription en direct
Récupérez le cable_token de /calls/initiate et abonnez-vous au CallsChannel via WebSocket pour recevoir les messages de transcription au fil de l'appel.