
Share:
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.
Text-to-Speech Voice Calls with PHP
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)
Composer for dependency management
Node.js and npm to install the Vonage CLI
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/phpdotenvEnsure 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
Add call recording using the record NCCO action and the Voice API recording guide
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
Explore use cases and structured learning paths in the Vonage Learning Center
Watch tutorials on our Vonage Developer YouTube Channel
Stay connected
Subscribe to the Vonage Developer Newsletter
Follow us on X for updates
Connect with us on LinkedIn
Help us improve our developer experience by filling out our Voice of the Developer Feedback
Share:
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.