Un seul processus. Tous vos SDA, tous vos trunks.
Strowger est entièrement piloté via une API HTTP légère : lancez des appels sortants, autorisez et configurez les appels entrants, recevez les CDR, gérez les trunks et les routes. Voici le contrat externe — payloads, codes d'état et points de vigilance opérationnels.
- Bind : loopback par défaut (
127.0.0.1:7060). Ne l'exposez jamais publiquement — placez-le derrière un reverse proxy TLS ou un tunnel si l'appelant est distant. - Auth : en-tête
X-API-Keysur chaque point de terminaison sauf/health,/metrics,/stats,/dashboard. Clé vide = authentification désactivée (dev uniquement). - Idempotence :
POST /callsaccepte uncall_idfourni par le client ; un identifiant déjà connu renvoie409. La génération d'identifiants uniques incombe au client. - Secrets : tout
api_key/passworddans un payload est en écriture seule — supprimé du stockage, jamais renvoyé, jamais journalisé.
| Méthode & chemin | Objectif |
|---|---|
POST /calls | Lancer un appel sortant (un POST = une tentative) |
GET /calls | Lister les appels actifs (?state=active) |
GET /calls/:id | État + tentatives pour un appel |
DELETE /calls/:id | Annuler (en file/sonnerie) ou raccrocher (BYE + finalisation normale) |
GET /cdrs | Historique CDR paginé (?from=&to=&endpoint=) |
GET·PUT·DELETE /endpoints | Gestion des trunks à chaud (enregistrement / auth IP) |
GET·PUT·DELETE /inbound_routes | Routage SDA entrant à chaud |
GET /health · /stats · /dashboard · /metrics · /trunks | Observabilité (pas d'auth sur les quatre premiers) |
Un POST correspond à une tentative. Il n'y a pas de relance interne dans Strowger — le client effectue un nouveau POST s'il le souhaite, en utilisant le status + sip_code du callback pour décider.
{
"call_id": "uuid-du-client", // idempotence (409 si déjà connu)
"destination": "+33612345678",
"trunk": "ovh-prod", // défaut : le trunk unique/par défaut
"caller_id": "+33184163651", // optionnel (voir piège MAN ci-dessous)
"agent_id": "lea", // étiquette libre, enregistrée dans le CDR
"backend": "gemini", // "gemini" (défaut) | "openai" (v2)
"model": "gemini-3.1-flash-live-preview",
"api_key": "…", // optionnel ; utilise la clé de config par défaut
"language": "fr",
"objectives": "…system prompt…",
"first_message": "Bonjour, …",
"tools": [ { "name": "…", "description": "…", "inputSchema": {…} } ],
"proxy_url": "http://…/proxy", // exécution des outils
"proxy_tools": ["send", "call_status"],
"audio": { // bruit ambiant (optionnel)
"background_noise": "office", "background_gain": 0.15,
"activity_noise": "keyboard", "activity_gain": 0.3
},
"rtp_timeout_secs": 30, // watchdog RTP (0 = désactivé)
"recording": false,
"callback_url": "http://…/callback?token=…"
}
| Code | Signification |
|---|---|
202 | {"call_id","status":"queued"} — accepté, l'appel est lancé |
400 | validation du payload |
409 | call_id déjà connu (idempotence) |
422 | trunk inconnu / non enregistré / désactivé |
503 | max_concurrent_calls atteint (limite physique du démon) |
La limite de simultanéité métier ("ce client peut passer N appels") réside dans l'application consommatrice, pas dans Strowger. Le 503 signale uniquement une saturation physique.
Pour chaque INVITE entrant, Strowger appelle le webhook du client (configuré par SDA). Le webhook est le point unique d'autorisation et de configuration. Son statut HTTP correspond à une réponse SIP :
| Réponse Webhook | SIP renvoyé | Signification |
|---|---|---|
| HTTP 200 + config JSON | answer | autorisé (config de session ci-dessous) |
| HTTP 404 | SIP 404 Not Found | ce numéro n'existe pas |
| HTTP 403 | SIP 603 Decline | appelant explicitement refusé |
| HTTP 429 | SIP 486 Busy Here | simultanéité max (côté client) |
| HTTP 503 | SIP 480 Temporarily Unavailable | service indisponible |
| autre (400, timeout, 5xx, mauvais JSON) | SIP 486 | ne jamais décrocher "juste pour voir" |
Corps du 200 — mêmes champs que POST /calls, sans destination/trunk :
{
"backend": "gemini", "model": "…",
"objectives": "…", "first_message": "…", "language": "fr",
"tools": [ … ], "proxy_url": "http://…/proxy",
"callback_url": "http://…/callback?token=…",
"recording": false
}
Séquençage de réponse : sur un 200, la session IA est établie d'abord (websocket + configuration confirmée) ; le SIP 200 OK est envoyé uniquement quand l'IA est prête. L'appelant n'entend jamais de blanc, et un backend IA hors service ne décroche jamais → zéro facturation. Gardez le webhook rapide : il est sur le chemin critique de réponse (webhook_timeout_secs, par défaut 5 s → 486 au-delà).
Correspondance SDA (multi-format)
Le champ did d'une route accepte trois formes, comparées après normalisation aux chiffres uniquement (+, espaces, points, tirets et paramètres URI supprimés) :
| Motif | Signification |
|---|---|
+33184163651 | correspondance exacte (après normalisation) |
*184163651 | correspondance par suffixe (≥ 6 chiffres) ; 9 chiffres = numéro national FR, invariant selon la présentation |
* | catch-all pour le trunk |
Résolution déterministe : exact > suffixe le plus long > *. La logique de correspondance est implémentée une seule fois, dans Strowger ; le webhook reçoit le did brut ainsi que le motif route correspondant comme clé de jointure pour le client.
À la fin de chaque appel, Strowger effectue un POST vers l'URL callback_url fournie. Aucun en-tête d'auth n'est ajouté — placez votre secret dans l'URL. Livraison : 3 tentatives avec backoff ; chaque tentative est journalisée (le CDR en base de données fait foi).
{
"call_id": "…", "direction": "outbound|inbound",
"status": "completed|failed|no_answer|busy|rejected",
"sip_code": 486,
"destination": "…", "caller_id": "…", "agent_id": "…",
"trunk": "ovh-prod", "backend": "gemini", "model": "…",
"cdr": { /* CDR complet, voir ci-dessous */ },
"transcription": "…", "tool_results": [ … ],
"recording": { // si recording=true
"urls": { "mixed": "http://…/recordings/{call_id}/mixed.wav?token=…",
"in": "…", "out": "…" },
"expires_at": "…" // rétention 24 h (configurable) puis purge
}
}
Les URLs d'enregistrement sont tokenisées et expirent — téléchargez le WAV avant expires_at.
Métriques télécom, quatre modes de sortie
Chaque appel terminé produit un CDR disponible via (1) SQLite (source de vérité, GET /calls/:id), (2) le callback, (3) un CSV tournant quotidiennement data_dir/cdr/YYYY-MM-DD.csv, et (4) GET /cdrs (JSON paginé).
| Champ | Signification |
|---|---|
setup_time | INVITE envoyé (sortant) / reçu (entrant) |
pdd_ms | Post-Dial Delay : INVITE → première réponse provisoire (18x) |
ring_secs | 18x → 200 OK (durée de sonnerie) |
answer_time · end_time | 200 OK (null si non répondu) · BYE/CANCEL/échec |
duration_secs · billsec | total · facturable (réponse → fin) |
disposition | ANSWERED / NO ANSWER / BUSY / FAILED / REJECTED |
released_by | caller / callee / api / watchdog_rtp / watchdog_silence / session_timer / shutdown / max_duration |
codec | PCMA / PCMU négocié |
rtp_in_packets · rtp_out_packets | compteurs média |
ttft_ms | métrique maison : premier audio agent (time-to-first-token) |
Un endpoint est une relation SIP avec un opérateur/PBX — soit REGISTER (identifiants), soit auth-IP (register: false, pas d'identifiants ; l'hôte entre dans la liste blanche, le keepalive OPTIONS devient le signal de disponibilité). Créez, inspectez, modifiez, drainez et supprimez des trunks à chaud, sans redémarrage. L'état expose registration, liveness, last_options_rtt_ms, calls_in_progress. Le champ password est en écriture seule.
Le pendant routage de /endpoints : provisionnez des routes SDA entrantes par API, visibles dès l'INVITE suivant, sans rechargement TOML. Upsert idempotent basé sur (trunk, normalised did) : 201 à la création, 200 au remplacement, 409 en cas de collision de motif. Lorsqu'une route API et une route de config partagent la même clé exacte, la route API l'emporte (la route de config est masquée et réapparaît si la route API est supprimée).
Le démon sert sa propre observabilité — rien à installer :
GET /health— état global (endpoints, files d'attente, backend IA joignable).GET /stats— JSON structuré : enregistrements par état, appels actifs, TTFT p95, perte RTP, underruns, dispositions, derniers CDR.GET /dashboard— page HTML autonome (auto-refresh), lit/stats.GET /metrics— format Prometheus, séries préfixéessipvox_.GET /trunks— enregistrement par trunk, expiration, disponibilité, appels en cours.
- Caller-ID & MAN (France, depuis 2026-01) : le CLI présenté doit être un numéro natif du trunk émetteur. Présenter le numéro d'un autre opérateur → l'appel est coupé avant d'atteindre l'appelé (attestation C du mécanisme d'authentification des numéros, ≈ STIR/SHAKEN). Sur un trunk OVH, conservez la valeur par défaut.
- Plages RTP disjointes : si un autre softswitch tourne sur la même machine, conservez des plages RTP séparées (Strowger utilise par défaut
22000-22999). Une collision se manifeste par des "non-réponses" massives. - Watchdog RTP : un appel sans RTP entrant pendant
rtp_timeout_secs(par défaut 30 s) est raccroché (released_by=watchdog_rtp). Réglez sur0pour désactiver par appel. - Session timers (RFC 4028) : respectés côté UAS ; un
Session-Expires < 90reçoit un422 Min-SE: 90;Require: 100relreçoit un420(non supporté en v1).