Illustration of two smartphones with anonymous calls, and a shield labeled "Voice Proxy" between them, connected by lines.

Renvoyer un appel téléphonique via Voice Proxy avec Python et Starlette

Publié le October 15, 2026

Temps de lecture : 12 minutes

Utiliser un proxy vocal pour préserver la confidentialité de ses numéros personnels

Vous connaissez bien la situation : vous avez commandé un repas à livrer, mais le code d'accès à votre immeuble ne fonctionne pas, et le livreur doit donc vous appeler pour finaliser la commande. Ni vous ni le livreur ne souhaitez communiquer votre numéro de téléphone personnel à l'autre. C'est une question de confidentialité, mais aussi une question pratique. Les gens sont plus enclins à répondre à un numéro dont l'indicatif régional leur est familier qu'à un numéro inconnu. 

Le renvoi d'appel permet d'acheminer les appels via un numéro intermédiaire, ce qui permet de préserver la confidentialité des numéros de téléphone personnels. Ce modèle est appelé proxy vocal. 

Dans ce tutoriel, nous allons utiliser Starlette et Vonage pour créer un proxy vocal en Python. Starlette gère la logique serveur et Vonage s'occupe de la téléphonie. Poursuivez votre lecture pour en savoir plus sur chaque composant avant de terminer le tutoriel, ou passez directement au fichier README pour mettre le code en œuvre. 

Qu'est-ce que Starlette ?

Si vous êtes développeur Python, vous connaissez sans doute certains des frameworks les plus populaires, de Flask à Django en passant par FastAPI. Chacun d’entre eux apporte une structure et des choix de conception en matière de développement HTTP, mais ils partagent tous, à la base, un modèle fondamental consistant à acheminer les requêtes vers des gestionnaires et à renvoyer des réponses. Starlette est le framework léger ASGI qui alimente FastAPI et qui peut être utilisé directement lorsque vous avez besoin d’un framework web minimaliste mais entièrement asynchrone, sans toutes les fonctionnalités supplémentaires. 

Qu'est-ce que l'ASGI et comment Starlette en tire-t-elle parti ?

L'interface ASGI (Asynchronous Server Gateway Interface) est une spécification (ou convention d'appel) permettant aux serveurs web de transmettre des requêtes à des frameworks Python prenant en charge l'asynchronisme. On peut considérer l'ASGI comme un contrat définissant la manière dont les serveurs web interagissent avec les applications Python. La signature de l'interface se présente comme suit : 

async def app(scope, receive, send): 
	pass 

Les paramètres de cette interface sont définis comme suit : 

  • scope Il s'agit d'un dictionnaire de métadonnées décrivant la requête reçue. Ce paramètre permet de déterminer le type de connexion à établir afin que la communication entre un serveur et une application Python puisse s'effectuer correctement. 

  • receive est une fonction permettant d'accepter et de traiter les événements provenant d'un client. Elle renvoie un dictionnaire représentant les événements qu'elle reçoit. 

  • send est une fonction qui renvoie des événements au client. C'est cette fonction qui est appelée pour générer et envoyer une réponse. 

Voici donc à quoi ressemble un exemple d'application ASGI complète :

async def app(scope, receive, send): 

    # Determine the type of connection from the `scope` metadata 
    if scope["type"] == "http" and scope["method"] == "POST": 

        # You need the body data for `send()`so call `receive()` 
        body_event = await receive() 

        body = body_event["body"] 
         
        # Parse and use the body data 
        data = json.loads(body) 

        # Then send response 
        await send({ 
            "type": "http.response.start", 
            "status": 200, 
            "headers": [[b"content-type", b"application/json"]], 
        }) 

        await send({ 
            "type": "http.response.body", 
            "body": json.dumps({"received": data}).encode(), 
        }) 

C'est ce modèle que Starlette abstrait pour vous éviter d'avoir à écrire chaque contrat à la main. Sans Starlette, votre application web devrait gérer le routage à l'aide de plusieurs niveaux d'instructions if/else : 

async def app(scope, receive, send):

    if scope["type"] == "http":

        path = scope["path"]
        method = scope["method"]

        # Manual routing logic
        if path == "/users" and method == "GET":
            await send(
                {
                    "type": "http.response.start",
                    "status": 200,
                    "headers": [[b"content-type", b"application/json"]],
                }
            )
            await send(
                {
                    "type": "http.response.body",
                    "body": b'[{"id": 1, "name": "Alice"}]',
                }
            )

        elif path == "/users/1" and method == "GET":
            await send(
                {
                    "type": "http.response.start",
                    "status": 200,
                    "headers": [[b"content-type", b"application/json"]],
                }
            )
            await send(
                {
                    "type": "http.response.body",
                    "body": b'{"id": 1, "name": "Alice"}',
                }
            )

        else:
            await send(
                {
                    "type": "http.response.start",
                    "status": 404,
                    "headers": [[b"content-type", b"text/plain"]],
                }
            )
            await send(
                {
                    "type": "http.response.body",
                    "body": b"Not Found",
                }
            )

Starlette dissimule toutes les redondances liées aux codes standard dans une API épurée : 

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

async def list_users(request):
    return JSONResponse([{"id": 1, "name": "Alice"}])

async def get_user(request):
    user_id = request.path_params["user_id"]
    return JSONResponse({"id": user_id, "name": "Alice"})

routes = [
    Route("/users", list_users, methods=["GET"]),
    Route("/users/{user_id}", get_user, methods=["GET"]),
]

app = Starlette(routes=routes)

Le code ci-dessus peut être décomposé en plusieurs éléments constitutifs : 

  • L' Route objet associe un chemin d’URL et une méthode HTTP à une fonction. Dans cet exemple, "/users" et les GET méthode sont associées à la list_users fonction. 

  • L' Request objet que les deux list_users et get_user reçoivent en paramètre contient les métadonnées relatives à la connexion entrante (la portée) ainsi que la fonction qui traite le message reçu du client (la fonction d’appel de réception). 

  • L' JSONResponse objet traite le message qui sera renvoyé au client (la fonction d'envoi). 

Comme Starlette gère le routage et le traitement des requêtes/réponses sans intégrer de couches supplémentaires telles que l'ORM, l'authentification ou les modèles de page, il constitue la solution idéale pour un projet ciblé comme ce proxy vocal.  

Vous pouvez consulter le fichier README du projet README pour découvrir un exemple d'application Starlette minimale. Pour en savoir plus sur Starlette, consultez la documentation. 

Qu'est-ce qu'un proxy vocal et comment fonctionne-t-il ?

Un proxy vocal (également appelé « masquage d'appel ») est une méthode permettant d'acheminer des appels téléphoniques vers leur destinataire via un numéro de téléphone intermédiaire. Le numéro de téléphone final est masqué derrière le numéro intermédiaire, ce qui garantit la confidentialité de ce dernier. Chaque participant reçoit un numéro temporaire lui permettant de communiquer directement en temps réel sans divulguer le numéro personnel de l'une ou l'autre des parties. Une fois l'appel terminé, le numéro intermédiaire est libéré et peut être réutilisé pour un autre proxy.  

Lorsqu'une personne appelle un numéro de proxy vocal, l'appel est pris en charge par une application qui le transfère ensuite vers le numéro réel du destinataire sans jamais le révéler à l'appelant. De même, la personne qui reçoit l'appel ne voit que le numéro intermédiaire et non le numéro réel de l'appelant, ce qui permet de préserver l'anonymat mutuel des numéros de téléphone. 

Le masquage des appels s'avère utile dans les scénarios de commerce entre particuliers, comme celui décrit précédemment dans ce tutoriel, et peut aider à suivre et à contrôler les appels et les transactions. Avec Vonage Voice et le SDK Python de Vonage, vous pouvez mettre en œuvre le masquage d'appel grâce à une API qui dissimule toute la complexité de l'infrastructure téléphonique. 

Objets de contrôle des appels et de la voix Vonage (NCCO)

L' Voice API Vonage vous permet de mettre en relation des personnes partout dans le monde et d’automatiser les interactions vocales qui enrichissent l’expérience utilisateur de votre produit. Vous pouvez gérer les appels vocaux par programmation à l’aide de deux modèles complémentaires : 

  • L'une d'entre elles est lancée par votre application, à l'aide d'appels à l'API REST ou de SDK. 

  • L'autre est demandé à votre application par Vonage, à l'aide de webhooks et d'objets de contrôle des appels (NCCO). 

Cette double approche permet une gestion flexible des appels afin de mettre en œuvre divers flux d'appels et cas d'utilisation de la Voice programmables, allant de simples notifications à des flux sophistiqués de centres de contact, en passant par l'intégration de l'IA. 

Un NCCO est une structure basée sur JSON qui définit ce qui doit se passer pendant un appel. Vous la renvoyez en réponse à la requête de webhook de Vonage lorsque l'appel est établi. 

Parmi les actions courantes menées par la NCCO, on peut citer : 

  • talk: Convertir le texte en parole et le faire écouter à l'appelant 

  • stream: Lire un fichier audio 

  • record: Enregistrer l'audio de l'appel 

  • connect: Ajouter un autre participant à la conversation 

Pour cette application, nous allons utiliser la connect action pour acheminer l'appel vers différents points de terminaison. Un point de terminaison peut être un autre numéro de téléphone, un WebSocket ou un point de terminaison SIP. Dans ce tutoriel, nous allons nous connecter à un numéro de téléphone afin de masquer les numéros d'origine des deux côtés de l'appel. 

Consultez l' Voice API et documentation du NCCO pour en savoir plus. 

Creusement de tunnels

Comme nous allons exécuter l'application Starlette en local, nous devons trouver un moyen de la rendre accessible à Vonage via une URL publique. C'est là qu'intervient le tunneling. Le tunneling permet d'exposer les serveurs locaux à l'Internet public via des URL publiques temporaires ou statiques. ngrok est une plateforme logicielle qui fournit ce service. 

Pour en savoir plus sur l'utilisation de ngrok avec Vonage, consultez la documentation. 

Tutoriel : Transférer un appel téléphonique via un proxy vocal avec Python, Starlette et Vonage

Dans ce tutoriel, nous allons utiliser Starlette pour définir un webhook que l' Voice API pour envoyer une requête, puis créer un proxy vocal à l’aide d’une action NCCO connect . Le déroulement est le suivant : 

  1. L'utilisateur final compose un numéro virtuel Vonage. 

  2. Vonage envoie une requête au webhook défini sur le serveur Starlette. 

  3. Le serveur renvoie une connect action NCCO. 

  4. Vonage transfère l'appel vers le numéro de destination configuré. 

  5. Aucune des deux parties ne voit les numéros de téléphone réels : elles ne voient que le numéro virtuel. 

Consultez le code complet de ce tutoriel sur GitHub et suivez le fichier fichier README pour le faire fonctionner. Nous aborderons ci-dessous les parties importantes du code. 

Conditions préalables

Configuration du tutoriel

Créer une application Vonage Voice 

Vous devrez créer une application Vonage Voice à utiliser dans le cadre de ce tutoriel. Pour ce faire, deux possibilités s'offrent à vous : 

Dans tous les cas, veillez à sélectionner et à associer un numéro compatible avec la fonction vocale à utiliser dans le cadre de ce tutoriel. 

Configurez votre environnement de développement 

Suivez les instructions figurant dans le fichier fichier README pour créer un environnement virtuel et installer les dépendances du projet : 

python -m venv venv 
source venv/bin/activate 

Et puis :

pip install -r requirements.txt

Pour en savoir plus sur les environnements virtuels, consultez notre article de blog. 

Configurez vos variables d'environnement

Copiez et renommez le env.template fichier : 

cp .env.template .env

Indiquez ensuite dans le fichier votre numéro virtuel Vonage ainsi que le numéro de l'appareil que vous utiliserez pour tester l'application de proxy vocal. Il s'agit du numéro auquel Vonage se connectera en utilisant un numéro virtuel afin de masquer le numéro d'origine : 

VONAGE_VIRTUAL_NUMBER=your-vonage-virtual-number
TEST_TO_NUMBER=your-test-number-for-voice-proxy

Lancer un tunnel ngrok 

Pour rendre votre application accessible à Vonage, utilisez ngrok afin de créer un tunnel vers une URL publique sur le port 8000: 

ngrok http 8000

Pour en savoir plus sur l'utilisation de ngrok avec les applications Vonage, consultez notre article de blog. 

Définissez et configurez vos webhooks avec Starlette et Vonage

L'application Starlette se trouve dans le server.py fichier. Grâce à l’abstraction de tout le code standard d’ASGI, Starlette nous permet de créer un proxy vocal en moins de 100 lignes de code, tandis que Vonage gère la complexité de la téléphonie via une API s’appuyant sur un SDK basé sur Pydantic.  

Nous définissons ici une VoiceProxy classe qui étend la HTTPEndpoint . Cette classe nous permet de configurer des gestionnaires HTTP sous forme de méthodes d’une même classe, plutôt que d’écrire une fonction unique nécessitant de vérifier manuellement la méthode de requête.  

class VoiceProxy(HTTPEndpoint):
    """A Starlette application that defines webhook endpoints
    for Vonage to make a request to when the Vonage virtual number is called"""

    async def get(self, request):

        from_number = request.query_params.get("from")
        to_number_endpoint = PhoneEndpoint(number=TEST_TO_NUMBER)

        logger.info(
            f"Received call to: {VONAGE_VIRTUAL_NUMBER} from: {from_number} -- creating voice proxy to: {TEST_TO_NUMBER}"
        )

        greeting = (
            "Hello! Voice proxy in process. Check logs for details. "
            "You should notice that the from number uses the intermediary virtual number."
        )

        ncco: list[NccoAction] = [
            Talk(text=greeting),
            Connect(endpoint=[to_number_endpoint], from_=VONAGE_VIRTUAL_NUMBER),
        ]

        return JSONResponse(
            [action.model_dump(by_alias=True, exclude_none=True) for action in ncco]
        )

    async def post(self, request):
        event = await request.json()

        uuid = event.get("uuid")
        from_number = event.get("from")
        to_number = event.get("to")
        direction = event.get("direction")
        status = event.get("status")

        logger.info(
            f"Voice proxy for uuid: {uuid} -- {direction} TO number: {to_number} FROM number: {from_number} -- STATUS: {status}"
        )

        return PlainTextResponse()

La GET méthode est chargée de renvoyer l'action NCCO qui crée le masquage d'appel avec connect. Dans ce cas précis, nous avons connect configuré le système pour qu’il utilise un point de terminaison de numéro de téléphone. Il s’agit du numéro qui recevra l’appel masqué provenant du from , qui, dans ce scénario, correspond à votre numéro virtuel Vonage : 

    async def get(self, request):


        from_number = request.query_params.get("from")
        to_number_endpoint = PhoneEndpoint(number=TEST_TO_NUMBER)


        logger.info(
            f"Received call to: {VONAGE_VIRTUAL_NUMBER} from: {from_number} -- creating voice proxy to: {TEST_TO_NUMBER}"
        )


        greeting = (
            "Hello! Voice proxy in process. Check logs for details. "
            "You should notice that the from number uses the intermediary virtual number."
        )


        ncco: list[NccoAction] = [
            Talk(text=greeting),
            Connect(endpoint=[to_number_endpoint], from_=VONAGE_VIRTUAL_NUMBER),
        ]


        return JSONResponse(
            [action.model_dump(by_alias=True, exclude_none=True) for action in ncco]
        )

Cette méthode correspond au webhook auquel Vonage enverra une requête lorsqu'un appel sera passé vers votre numéro virtuel. 

Les applications Vonage Voice peuvent également être configurées avec un webhook d'événement. Il s'agit d'un point de terminaison vers lequel les requêtes sont envoyées lorsque des événements Voice se produisent. Dans notre application de proxy vocal, nous utilisons la post méthode pour consigner notre workflow Voice et illustrer le masquage d'appel en action : 

    async def post(self, request):
        event = await request.json()


        uuid = event.get("uuid")
        from_number = event.get("from")
        to_number = event.get("to")
        direction = event.get("direction")
        status = event.get("status")


        logger.info(
            f"Voice proxy for uuid: {uuid} -- {direction} TO number: {to_number} FROM number: {from_number} -- STATUS: {status}"
        )


        return PlainTextResponse()

Enfin, nous enregistrons nos itinéraires et les transmettons à l'application Starlette : 

routes = [
    Route("/", VoiceProxy),
]


app = Starlette(routes=routes)

Lancez l'application à l'aide d'uvicorn :

uvicorn server:app --reload

Configurez vos webhooks avec Vonage 

Pour que Vonage puisse accéder à votre application, utilisez l'URL publique générée par ngrok. Modifiez votre application Voice dans le tableau de bord des développeurs Vonage de manière à ce que l' URL de réponse et URL d'événement pointent vers l'URL fournie par ngrok. Cela ressemblera à quelque chose comme ceci :

https://<your-ngrok-url>/

Veillez à définir les méthodes de requête appropriées pour chacune d'entre elles :  

  • GET pour l' URL de la réponse 

  • POST pour l' URL de l'événement 

Essayez-le !

Pour tester l'application, vous aurez besoin de deux appareils capables de passer et de recevoir des appels téléphoniques. 

  1. Sur l'appareil associé au numéro de téléphone qui n'est pas configuré pour être utilisé avec l’application, appelez votre numéro virtuel Vonage. 

  2. Vous devriez entendre un message d'accueil généré par synthèse vocale, suivi d'une sonnerie, pendant que Vonage appelle le numéro de test que vous avez indiqué dans l'application. 

  3. L'appareil associé au numéro de test devrait sonner, mais au lieu que l'appel provienne de l'appareil avec lequel vous avez passé l'appel initial, le numéro affiché devrait être votre numéro virtuel. En d'autres termes, le numéro virtuel Vonage masque votre numéro de téléphone privé. 

Vous pouvez consulter les journaux d'événements d'appel pour en avoir la confirmation :

INFO:server:Voice proxy for uuid: 90f693... -- inbound TO number: 12015551497 FROM number: 18055556289 -- STATUS: answered 

INFO:     216.147.2.112:0 - "POST / HTTP/1.1" 200 OK 

INFO:server:Voice proxy for uuid: 115e4... -- outbound TO number: 14155555833 FROM number: 12015551497 -- STATUS: ringing 

INFO:     216.147.2.112:0 - "POST / HTTP/1.1" 200 OK 

INFO:server:Voice proxy for uuid: 115e4... -- outbound TO number: 14155555833 FROM number: 12015551497 -- STATUS: started 

INFO:     216.147.2.13:0 - "POST / HTTP/1.1" 200 OK 

INFO:server:Voice proxy for uuid: 90f693... -- inbound TO number: 12016441497 FROM number: 18055556289 -- STATUS: completed 

INFO:     216.147.2.101:0 - "POST / HTTP/1.1" 200 OK 

INFO:server:Voice proxy for uuid: 115e4... -- outbound TO number: 14155555833 FROM number: 12015551497 -- STATUS: completed 

INFO:     216.147.2.101:0 - "POST / HTTP/1.1" 200 OK 

INFO:server:Voice proxy for uuid: 115e4... -- outbound TO number: 14155555833 FROM number: 12015551497 -- STATUS: cancelled 

Dans cet exemple : 

  • 12015551497 est le numéro de masquage intermédiaire. 

  • 18055556289 C'est le numéro qui passe l'appel. 

  • 14155555833 est le numéro auquel on se connecte à l'aide du numéro de masquage. 

Grâce à un serveur proxy, les appelants peuvent être mis en relation sans se dévoiler mutuellement leurs numéros de téléphone. Cela permet d'offrir un service client de type « pair à pair » tout en préservant la vie privée des clients.  

En résumé

Dans ce tutoriel, vous avez développé une application de proxy vocal en Python à l'aide de Starlette et de la Voice API de Vonage. Un proxy vocal — également appelé « masquage d'appel » — achemine les appels via un numéro virtuel intermédiaire, de sorte qu'aucune des deux parties ne puisse jamais voir le numéro de téléphone réel de l'autre. Vous avez découvert comment la boîte à outils ASGI légère de Starlette simplifie la gestion des webhooks, comment les objets de contrôle d’appel (NCCO) de Vonage définissent le comportement des appels, et comment ngrok expose votre serveur local à Vonage pendant le développement. À la fin, vous disposiez d’une application fonctionnelle qui relie deux appelants via un numéro virtuel Vonage tout en préservant la confidentialité des deux numéros de téléphone — un modèle qui trouve des applications concrètes dans les plateformes de livraison, de covoiturage, de marché en ligne et de service client. Pour en savoir plus sur la manière d’utiliser Vonage afin d’intégrer la voix dans vos applications et services, consultez nos parcours d’apprentissage sur la voix ou regardez la Video ci-dessous qui explique comment automatiser les appels grâce à l’IA. 

Lectures complémentaires et références

Vous avez une question ou souhaitez partager ce que vous construisez ?

Restez connecté et tenez-vous au courant des dernières nouvelles, astuces et événements concernant les développeurs.

Partager:

https://a.storyblok.com/f/270183/400x400/2c4345217d/liz-acosta.jpeg
Liz AcostaDéfenseur des développeurs

Liz Acosta est conseillère en développement chez Vonage. Si son parcours professionnel, d'étudiante en cinéma à spécialiste du marketing, puis ingénieur et enfin défenseur des développeurs, peut sembler peu conventionnel, il est tout à fait typique des relations avec les développeurs ! Liz aime les pizzas, les plantes, les carlins et Python.