Référence de l'API HTTP

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.

Conventions
  • 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-Key sur chaque point de terminaison sauf /health, /metrics, /stats, /dashboard. Clé vide = authentification désactivée (dev uniquement).
  • Idempotence : POST /calls accepte un call_id fourni par le client ; un identifiant déjà connu renvoie 409. La génération d'identifiants uniques incombe au client.
  • Secrets : tout api_key/password dans un payload est en écriture seule — supprimé du stockage, jamais renvoyé, jamais journalisé.
Points de terminaison
Méthode & cheminObjectif
POST /callsLancer un appel sortant (un POST = une tentative)
GET /callsLister les appels actifs (?state=active)
GET /calls/:idÉtat + tentatives pour un appel
DELETE /calls/:idAnnuler (en file/sonnerie) ou raccrocher (BYE + finalisation normale)
GET /cdrsHistorique CDR paginé (?from=&to=&endpoint=)
GET·PUT·DELETE /endpointsGestion des trunks à chaud (enregistrement / auth IP)
GET·PUT·DELETE /inbound_routesRoutage SDA entrant à chaud
GET /health · /stats · /dashboard · /metrics · /trunksObservabilité (pas d'auth sur les quatre premiers)
Sortant
POST/calls

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=…"
}
CodeSignification
202{"call_id","status":"queued"} — accepté, l'appel est lancé
400validation du payload
409call_id déjà connu (idempotence)
422trunk inconnu / non enregistré / désactivé
503max_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.

Entrant
GET{webhook_url}?did=&caller=&route=

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 WebhookSIP renvoyéSignification
HTTP 200 + config JSONanswerautorisé (config de session ci-dessous)
HTTP 404SIP 404 Not Foundce numéro n'existe pas
HTTP 403SIP 603 Declineappelant explicitement refusé
HTTP 429SIP 486 Busy Heresimultanéité max (côté client)
HTTP 503SIP 480 Temporarily Unavailableservice indisponible
autre (400, timeout, 5xx, mauvais JSON)SIP 486ne 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) :

MotifSignification
+33184163651correspondance exacte (après normalisation)
*184163651correspondance 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.

Callback de fin d'appel

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

CDR

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

ChampSignification
setup_timeINVITE envoyé (sortant) / reçu (entrant)
pdd_msPost-Dial Delay : INVITE → première réponse provisoire (18x)
ring_secs18x → 200 OK (durée de sonnerie)
answer_time · end_time200 OK (null si non répondu) · BYE/CANCEL/échec
duration_secs · billsectotal · facturable (réponse → fin)
dispositionANSWERED / NO ANSWER / BUSY / FAILED / REJECTED
released_bycaller / callee / api / watchdog_rtp / watchdog_silence / session_timer / shutdown / max_duration
codecPCMA / PCMU négocié
rtp_in_packets · rtp_out_packetscompteurs média
ttft_msmétrique maison : premier audio agent (time-to-first-token)
Trunks & routes
POSTGETPUTDELETE/endpoints

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.

GETPUTDELETE/inbound_routes

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

Observabilité

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ées sipvox_.
  • GET /trunks — enregistrement par trunk, expiration, disponibilité, appels en cours.
Pièges opérationnels
  • 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 sur 0 pour désactiver par appel.
  • Session timers (RFC 4028) : respectés côté UAS ; un Session-Expires < 90 reçoit un 422 Min-SE: 90 ; Require: 100rel reçoit un 420 (non supporté en v1).

Un pilote se déploie en une matinée : un binaire, un fichier de configuration, votre trunk.

STROWGER — AGENTS VOCAUX POUR OPÉRATEURS · PROPULSÉ PAR LE MOTEUR SIPVOX · DOCUMENT DE TRAVAIL, TARIFS INDICATIFS · NOM EN COURS DE VÉRIFICATION DE MARQUE (EUIPO/USPTO)