https://a.storyblok.com/f/270183/1368x684/0dc436b804/26oct-text_to_speech_voice_calls_with_php_r3.jpg

Text-to-Speech Voice Calls with PHP

Published on October 27, 2026

Time to read: 6 minutes

Introduction

Building voice-powered applications used to mean wrestling with hardware gateways and low-level telephony protocols. Today, the Vonage Voice API handles all that complexity with just a few lines of code. 

In this tutorial, you’ll build a small PHP application that handles both inbound and outbound phone calls using the Vonage Voice API. You’ll start by returning a text-to-speech (TTS) message to anyone who calls your Vonage number, then extend the app to programmatically trigger outbound calls. Finally, you’ll make the spoken message dynamic by fetching a random joke from a public API. 

Tl;dr: View the sample code in the Vonage Community GitHub repository. 

Prerequisites 

Before you start, make sure you have the following: 

  • PHP 8.3+ (8.3.36, 8.4.27, or 8.5.12 to avoid php2shell)

  • ngrok (or a similar tunnelling tool) to expose your local server to the internet 

Set Up the Vonage CLI 

The Vonage CLI lets you manage your account, create applications, and purchase numbers from the terminal. Install it globally with npm: 

npm install -g @vonage/cli 

Once installed, authenticate the CLI with your API key and secret, which you’ll find in the Vonage API Dashboard: 

vonage auth set --api-key=YOUR_API_KEY --api-secret=YOUR_API_SECRET

Create a Vonage Application 

A Vonage application is a container for the configuration and credentials your code needs to interact with Vonage APIs. Create a new working directory for your project, navigate into it, then run the following command: 

mkdir vonage-calls 
cd vonage-calls 

vonage apps create "vonage-calls"  

You have now created a Vonage Application. Note the Application ID returned; you’ll need it shortly. The CLI also saves a private.key file in your current directory; keep it safe, as we’ll use it alongside the Application ID to authenticate with the Voice API in later steps. 

Next, you need to purchase a voice-enabled virtual number to link to your application. Search for an available number in your country (replace US with your two-letter country code if needed). Numbers must be in E.164 format: include the country code and omit any leading zeros (for example, a UK number 07700900001 becomes 447700900001): 

vonage numbers search US --feature=VOICE 

Select a number from the returned list, then purchase it by running: 

vonage numbers buy US SELECTED_NUMBER_FROM_SEARCH 

Finally, link the number to your application: 

vonage apps numbers link YOUR_ APPLICATION_ID YOUR_VONAGE_NUMBER  

You can check that the number has been correctly linked to your application by running: 

Vonage apps numbers list YOUR_ APPLICATION_ID 

Set Up Your Workspace 

In your project directory, run composer init to start. Accept all defaults and skip defining your dependencies interactively; we’ll do this in the next step. 

This tutorial uses the Slim Framework to handle incoming webhook requests from Vonage. Next, install Slim along with its PSR-7 implementation and the phpdotenv library for managing environment variables: 

composer require slim/slim "^4.0" 
composer require slim/psr7 
composer require vlucas/phpdotenv

Ensure that the private.key file you received when creating your application is in this directory, at the same level as composer.json. 

Create a .env file in the project root to store your credentials: 

VONAGE_APPLICATION_ID=your-application-id 
VONAGE_NUMBER=your-vonage-number 
BASE_URL=https://YOUR_NGROK_URL 

Add .env and private.key to your .gitignore file so credentials are never committed to version control: 

.env 
private.key 

Now start the local PHP development server: 

php -t public -S localhost:8000 

Your application is now listening on port 8000.  

Next, you need to expose your application to the internet so that Vonage can reach your webhooks. Let’s use ngrok for it: 

ngrok http 8000 

Make note of your newly created ngrok URL; you’ll need to update your Vonage Application with it next. 

Head over to your Vonage Dashboard and find your “vonage-calls” application under the Applications tab. Click on the Edit button, switch the Voice toggle on under Capabilities, then fill in your webhook URLs as shown below: 

Answer URL: https://YOUR_NGROK_URL/webhooks/answer 

Event URL: https://YOUR_NGROK_URL/webhooks/event  

Save your changes, then you’re all set! 

Create Your NCCO 

Vonage Voice calls are controlled using JSON call control objects called NCCOs. An NCCO is a JSON array of actions that tells Vonage what to do when a call starts. Some of the available actions include: 

  • connect - connect a call to another number 

  • record - record a call 

  • conversation - create a conference call 

  • talk - generate a text-to-speech message 

See the NCCO reference for the full list. 

Here’s the NCCO you’ll start with; a simple text-to-speech message: 

[ 
  { 
    "action": "talk", 
    "text": "The amount of visible light from a lamp is measured in lumens." 
  } 
] 

Note on voice customisation: The talk action supports language and style parameters to control the voice used for text-to-speech. See the text-to-speech language reference for the full list of available voices. 

Build the Answer Webhook 

Create a file named public/index.php. The code below bootstraps the Slim app, loads your environment variables, registers an answer webhook handler, and returns the NCCO as a JSON response whenever Vonage makes a GET request to /webhooks/answer. 

It also registers an event webhook at /webhooks/event that Vonage sends call status updates (such as ringing, answered, and completed) to throughout the call lifecycle. For now, the handler simply acknowledges receipt with a 204 No Content response; in a production app, you’d log or act on these events. 

<?php 
 
use Psr\Http\Message\ServerRequestInterface as Request; 
use Psr\Http\Message\ResponseInterface as Response; 
use Slim\Factory\AppFactory; 
 
require __DIR__ . '/../vendor/autoload.php'; 
 
// Load environment variables from .env 
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../'); 
$dotenv->load(); 
 
$app = AppFactory::create(); 
 
$app->get('/webhooks/answer', function (Request $request, Response $response): Response { 
    $ncco = [ 
        [ 
            'action' => 'talk', 
            'text'   => 'The amount of visible light from a lamp is measured in lumens.', 
        ], 
    ]; 
 
    $response->getBody()->write(json_encode($ncco)); 
    return $response->withHeader('Content-Type', 'application/json'); 
}); 
 
$app->get('/webhooks/event', function (Request $request, Response $response): Response { 
    // Vonage sends call status updates (ringing, answered, completed) here. 
    // Log or handle them as needed. 
    return $response->withStatus(204); 
}); 
 
$app->run(); 

When Vonage makes a GET request to your Answer URL, the handler returns a JSON-encoded NCCO. As long as the response is valid JSON in the correct format, Vonage knows how to handle the call. 

Time to test it out! Save the file, then call the virtual number you purchased to hear your text-to-speech message. 

Make an Outbound Call 

You’ve handled an inbound call, now let’s trigger an outbound one. When you make an outbound call, Vonage still looks at your Answer URL to find out how to handle it, so the webhook you just built will serve both directions. 

To make an outbound call, we’ll need the Vonage PHP SDK: 

composer require vonage/client 

Now add the following use statements at the top of public/index.php, after the existing ones: 

use Vonage\Client\Credentials\Keypair; 
use Vonage\Voice\OutboundCall; 
use Vonage\Voice\Endpoint\Phone; 
use Vonage\Voice\Webhook; 

Then add the route before $app->run(): 

$app->get('/makeCall/{number}', function (Request $request, Response $response, array $args): Response { 
    $keypair = new Keypair( 
        file_get_contents(__DIR__ . '/../private.key'), 
        $_ENV['VONAGE_APPLICATION_ID'] 
    ); 
 
    $client = new \Vonage\Client($keypair); 
 
    $outboundCall = new OutboundCall( 
        new Phone($args['number']), 
        new Phone($_ENV['VONAGE_NUMBER']) 
    ); 
 
    $outboundCall->setAnswerWebhook( 
        new Webhook($_ENV['BASE_URL'] . '/webhooks/answer', Webhook::METHOD_GET) 
    ); 
 
    $client->voice()->createOutboundCall($outboundCall); 
 
    $response->getBody()->write(json_encode(['status' => 'call initiated'])); 
    return $response 
        ->withHeader('Content-Type', 'application/json') 
        ->withStatus(200); 
}); 

To trigger an outbound call, visit the following URL in your browser, replacing DESTINATION_NUMBER with the number you want to call in E.164 format (for example, 14155550100 for a US number): 

https://YOUR_NGROK_URL/makeCall/DESTINATION_NUMBER 

Add Dynamic NCCOs 

You’ve accomplished what you set out to do, but it gets a little repetitive with the same message playing every time. Let’s make it dynamic by fetching a random programming joke from JokeAPI and reading it out on the call instead. 

To make HTTP requests, install Guzzle: 

composer require guzzlehttp/guzzle 

Add the Guzzle use statement near the top of index.php: 

use GuzzleHttp\Client as GuzzleClient; 

Now update the /webhooks/answer handler to fetch a joke and use it as the spoken text: 

$app->get('/webhooks/answer', function (Request $request, Response $response): Response { 
    $guzzle = new GuzzleClient(); 
    $apiResponse = json_decode( 
        $guzzle->get('https://v2.jokeapi.dev/joke/Programming?type=twopart&safe-mode')->getBody() 
    ); 
 
    // JokeAPI returns a two-part joke with a 'setup' and 'delivery' field. 
    $joke = $apiResponse->setup . ' ' . $apiResponse->delivery; 
 
    $ncco = [ 
        [ 
            'action' => 'talk', 
            'text'   => $joke, 
        ], 
    ]; 
 
    $response->getBody()->write(json_encode($ncco)); 
    return $response->withHeader('Content-Type', 'application/json'); 
}); 

JokeAPI is an actively maintained, free API. The type=twopart parameter returns jokes with setup and delivery fields, and safe-mode filters out explicit content. The handler concatenates them into a single string for the talk action to read aloud. 

Now any time Vonage requests your Answer URL, whether for an inbound call or an outbound one you triggered, it will fetch a fresh programming joke and read it out. Test it by calling your Vonage number or by hitting the /makeCall endpoint. 

What’s Next? 

You’ve built a text-to-speech voice call system that handles both inbound and outbound calls and delivers dynamic content.

From here, you could: 

  • Build a voice alert system - loop through a list of contacts, broadcast calls, and ask recipients to press a key to confirm receipt 

  • Customise the message based on who’s calling, the time of day, or data from your own database 

  • Explore the full NCCO reference to add IVR menus, conference calls, and more 

Want to learn, stay connected, or share what you're building?

Keep learning

Stay connected

Share:

https://a.storyblok.com/f/270183/372x373/36054b72d0/julia-biro.png
Julia BiroDeveloper Advocate

Julia is committed to empowering fellow developers by creating tutorials, guides, and practical resources. With a background in outreach and education, she aims to make technology more accessible and enhance the overall developer experience. You can often find her at local community events.