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.

Le plus rapide

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é.

Contrôle total

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 designPhonevoiceVous
Effort d'intégrationColler une iframeGérer des webhooks
Lecture audioIntégréeVia recording_url
Où vivent les données d'appelHébergées par PhonevoiceChez 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=.

🔒 Vous construisez une UI navigateur ou embarquée ? N'envoyez jamais votre token de projet ou utilisateur au client. Générez côté serveur une URL signée de courte durée avec /calls/:id/embed_session ou /calls/:id/player_session et ne transmettez que celle-ci au navigateur — elle expire en quelques minutes et n'ouvre rien d'autre.

Erreurs & codes de statut

Les erreurs renvoient un corps JSON avec un message « error » et un statut HTTP correspondant.

CodeMeaning
200 / 201OK / created
401Token manquant ou invalide
403Authentifié mais vous ne possédez pas cette ressource
404Introuvable
422Erreur 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/initiateDémarrer un appel sortant
GET/api/v1/agentsLister vos agents (utilisez un id comme agent_id dans /calls/initiate)
POST/api/v1/calls/:id/hangupRaccrocher un appel
POST/api/v1/calls/:id/add_monitorFaire entrer un superviseur, micro coupé (écoute en direct)
POST/api/v1/calls/:id/embed_sessionURL d'iframe signée pour le softphone navigateur / l'écoute
POST/api/v1/calls/:id/player_sessionURL d'iframe signée pour rejouer l'enregistrement + la transcription
GET/api/v1/calls/:id/costsDé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>
roleUsage
agentLe softphone du vendeur — micro ouvert ; démarre, enregistre et termine l'appel ; appelle le client.
talkMicro 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é.

ℹ️ Même captation que le relais — les appels bridge sont enregistrés (bicanal), transcrits en direct puis re-calés avec diarisation après l'appel, et résumés. Vous recevez phone_call.started / phone_call.completed (le transcript complet est dans le payload completed — pas d'événement transcript par message). Le lecteur d'enregistrement intégré fonctionne aussi ici via /calls/:id/player_session.

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 scriptSignification
data-keyObligatoire. 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-modesListe 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-linksMettez « off » pour désactiver la détection automatique des liens tel: (boutons explicites uniquement).
data-apiSurcharge 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-phoneLe numéro à appeler (E.164 de préférence).
data-pv-nameNom du contact affiché dans la modale et attaché à l'appel.
data-pv-contactedMettez « false » pour masquer le numéro sur le bouton (affiche « Numéro masqué »).
data-pv-type / data-pv-idMé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.

🔒 Ne mettez jamais un token de projet ou utilisateur dans une page — ce sont des secrets (voir Authentification). Le widget n'a besoin que de la clé publiable.

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/configLa configuration publique de la clé (modes, fonctionnalités, objectif prérempli)
POST/api/widget/sessionsCréer une session d'appel (renvoie un token de session de courte durée)
GET/api/widget/calls/:idStatut, 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. 1 — Trouver & acheter un numéro

    Cherchez des numéros disponibles, puis achetez-en un et définissez vers où il doit renvoyer.

  2. 2 — Configurer le renvoi

    Changez la destination ou suspendez le relais à tout moment avec PATCH.

  3. 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)
Renvoi des SMS — les SMS entrants sont renvoyés et déclenchent un webhook sms.received, mais uniquement sur les numéros compatibles SMS. Les numéros fixes/géographiques FR ne sont généralement PAS compatibles SMS ; le relais voix fonctionne sur tous.
Enrichissement progressif — phone_call.completed peut se déclencher plusieurs fois : d'abord au raccrochage, à nouveau quand le transcript est prêt, encore après le résumé IA. Considérez le dernier payload comme faisant foi (faites correspondre sur data.id).

Numéros

GET/api/v1/numbersLister vos numéros + config de relais
GET/api/v1/numbers/availableChercher des numéros achetables (country, type, area_code, contains)
POST/api/v1/numbersAcheter un numéro (définir forward_to pour l'utiliser en relais)
PATCH/api/v1/numbers/:idDé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/webhooksLister vos webhooks
POST/api/v1/webhooksCréer un webhook (renvoie le secret une seule fois)
PATCH/api/v1/webhooks/:idMettre à jour url / name / events / active
DELETE/api/v1/webhooks/:idSupprimer un webhook
GET/api/v1/webhooks/:id/callsJournal 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

typeSe déclenche quand
phone_call.startedL'appel se connecte (en cours).
phone_call.completedL'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.transcriptChaque message de transcription, en temps réel (appels agent IA).
sms.receivedUn 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.

Renvoyer un vrai numéro, tout capter

Achetez un numéro relais, pointez votre ligne existante dessus, et recevez l'enregistrement, la transcription, le résumé et les SMS sur vos webhooks — sans application ni agent IA.