https://a.storyblok.com/f/270183/1368x665/c3dd68d086/26aug_dev-blog_receiving-sms-delivery-receipts-with-php.jpg

Receiving SMS Delivery Receipts with PHP and Vonage

最終更新日 August 13, 2026

所要時間:6 分

Delivery receipts for a sent SMS are a great source of data that allows developers to track transit. This is particularly useful if the quality of the numbers you are sending requires a high success rate, you have a production system that has scaled to a point where undelivered SMS API requests become costly to the system, or you are running a campaign and need to track the results.

Receipts are sent as webhooks when the end device receives the payload (DELIVRD), or the carrier has received the SMS and posted it to the device (ACCEPTD). There is an additional option to enable “Treated as delivered”, which is useful when the routed carrier does not issue receipts.

This data gives you far more visibility of your messaging stack; in this tutorial, we are going to build an implementation to read Delivery Reports (DLR’s) using the Slim framework in PHP.

Prerequisites

Create a Slim Application

Firstly, you will need to create a new Composer project and then fetch Slim.

mkdir dlr-reports && cd dlr-reports

composer init

Follow the interactive CLI instructions, and you can use the defaults for everything, apart from interactively choosing your packages. You should now have a composer.json file: in my case, it looks like this:

{
   "name": "j-seconde/dlr-reports",
   "autoload": {
       "psr-4": {
           "JSeconde\\DlrReports\\": "src/"
       }
   },
   "authors": [
       {
           "name": "Jim Seconde",
           "email": "test@test.com"
       }
   ],
   "require": {}
}

Now, we add Slim, Slim’s PSR-7 compatible HTTP handling, and the Vonage PHP SDK:

composer require slim/slim

composer require slim/psr7

composer require vonage/client-core

Adding a Webhook Route

The route will accept a POST request that will contain delivery receipts in the payload. In the Vonage Dashboard, you can actually choose whether to receive a POST or GET request, but we’ll go with POST as a more standard way of delivering data.

touch src/index.php

And then add our code into the new file:

<?php

use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;

require 'vendor/autoload.php';

$app = AppFactory::create();

$handler = function (Request $request, Response $response) {
    return $response->withStatus(204);
};

$app->map(['POST'], '/webhooks/sms', $handler);
$app->run();

The next step is that we want some kind of evidence that the application has received a Webhook, which we can’t do by responding to the Vonage request. The two ways we can do this are either using a Logger, or setting up a database to persist the Webhook data. To make things a little easier, we’ll go with using a Logger that can show the payload, which we can also use to execute some logic on the delivery receipt and output a value to it. We’re going to use Monolog to do this:

composer require monolog/monolog

Next, we implement hydrating the Webhook and writing it to our log file in the $handler:

<?php

use Monolog\Level;

use \Psr\Http\Message\ServerRequestInterface as Request;

use \Psr\Http\Message\ResponseInterface as Response;

use Slim\Factory\AppFactory;

use Monolog\Logger;

use Monolog\Handler\StreamHandler;

require 'vendor/autoload.php';

$app = AppFactory::create();
$logger = new Logger('sms');
$logger->pushHandler(new StreamHandler(__DIR__ . '/../log.txt', Level::Info));

$handler = function (Request $request, Response $response) use ($logger) {
   $logger->info('Delivery Receipt Receieved:' . $sms->toArray());
   return $response->withStatus(204);
};

The app is ready to go: run it using PHP’s built-in web server:

php -S 127.0.0.1:8000 -t src/

Installing the Vonage CLI

Using a JavaScript Package Manager such as npm or yarn, install the Vonage CLI globally:

npm install -g @vonage/cli

Two configurations will now need to be completed: you will need to set up your API keys with ngrok as part of the setup process, and follow the Vonage setup instructions for the Vonage CLI.

Creating an App

We’ll need a Vonage application instance to both authenticate our outgoing SMS messages and to route our webhooks back to our app. This will all be taken care of by the Vonage CLI. Create an app in the terminal:

Firstly, create the app:

vonage apps create "My App"

Next, give it the Messages capability:

vonage apps capabilities update <your-app-id> messages \
--messages-inbound-url=https://example.com/webhooks/inbound \
--messages-status-url=https://example.com/webhooks/status

We’ve added example.com as a placeholder for now, because the CLI will handle our domains as you’ll see later.

Using the Vonage Tunnel

To allow Vonage to send webhooks to your Slim application, you'll need to expose your local development server to the internet. A simple way to do this is by using ngrok. While you can run ngrok directly, the Vonage CLI provides a convenient wrapper around it that streamlines the setup process and makes the overall development experience smoother.

You can use the tunnel feature to open your app up to receive webhooks with the following command:

vonage tunnel ngrok <your-app-id>

What this command does is quite useful. Rather than simply starting an ngrok tunnel to your application, it also updates your application's webhook configuration to use the temporary ngrok URL instead of your existing domain (for example, replacing https://example.com with https://3d3daF.ngrok.com).

It preserves the existing webhook paths and only swaps the domain, so endpoints such as /webhooks/inbound and /webhooks/status continue to work without any additional configuration.

When you stop the ngrok tunnel, the command automatically attempts to restore your original webhook URLs, returning your application to its previous configuration.

Configure Outgoing SMS

Next, we need to send a message. You could do this with a command by implementing a CLI such as the Symfony CLI, but it’ll be easier to hit a new POST route that takes the message to send and the number to send it to in the payload. We also need to install and configure the Vonage PHP SDK. The PHP SDK also needs a PSR-7-compatible HTTP Client, so we’ll install the Symfony HTTP Client first, then the Vonage PHP SDK.

composer require symfony/http-client

composer require vonage/client-core

A Send SMS Route

We need a new route to hit that sends an SMS, which means we need to create a controller (if we’re going to do things in a typical PHP Framework setup). These should go into their own folder:

mkdir src/Controller

touch ./src/Controller/SMSController.php

Now, wire the route in your index.php entry point:

// make sure you import the controller (this is an example for how my PSR-4 is working, replace yours appropriately)
use JSeconde\BlogMessagesDeliveryReceipts\Controller;

// then, at the bottom of the file
$app->get('/send', SMSController::send);

Inside this controller, we’re going to configure the Vonage client and send an SMS. When you fire this route, it will send, and then your app will receive delivery receipts in another new route. Open up the controller and code it:

public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
   {
       $params = $request->getQueryParams();
       $message = $params['message'] ?? null;

       if (!$message) {
           $response->getBody()->write(json_encode(['error' => 'message query parameter is required']));

           return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
       }

       $privateKey = file_get_contents(__DIR__ . '/../../private.key');
       $applicationId = $_ENV['VONAGE_APPLICATION_ID'];
       $vonage = new Client(new Keypair($privateKey, $applicationId));

       $sms = new SMSText(
           to: $_ENV['VONAGE_TO'],
           from: $_ENV['VONAGE_FROM'],
           message: $message
       );

       $result = $vonage->messages()->send($sms);
       $response->getBody()->write(json_encode(['message_uuid' => $result['message_uuid'] ?? null]));

       return $response->withHeader('Content-Type', 'application/json');
   }
}

This controller will only work once you have created an .env file with your application ID, a number to send from, and a number to send to. You’ll also need to put your private.key downloaded from the dashboard in the root folder of your code.

This code uses the PHP ENV library, so install it:

composer require vlucas/phpdotenv

Then create your .env file in the project root:

touch .env

Plug in your variables:

VONAGE_TO=<device-number>
VONAGE_FROM=VonageDemo
VONAGE_APPLICATION_ID=<your-app-id>

Hitting this controller will now send an SMS, which will fire a Webhook from Vonage to your tunnel, plus /webhooks/status as the path. We’ve not set that up yet, so add a new method to your SMSController and get it to dump the incoming delivery status to your log file.

public function receive(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
   $body = (string) $request->getBody();
   $logger->info('Webhook status received', json_decode($body, true) ?? ['raw' => $body]);

   return $response->withStatus(200);
}

The last thing to do is to wire your route together to hit the new controller method in your index.php :

$controller = new TestController($logger);

$app->get('/test', $controller);

$app->post('/webhook/status', [$controller, 'receive']);

All done. Hit your Send endpoint, and watch your delivery receipts roll into your webhooks.txt log file!

Conclusion

We’ve linked the sending and receiving of SMS data, but there is so much more you can do. For example, you can add routing logic based on the payload of the notification, so a failed message could be queued up to attempt a repeat sending. Alternatively, if you set your PHP application to persist data in large amounts, you could configure Event Sourcing using a library such as Laravel Verbs to record and replay your sending history with individual numbers.

ご質問がある場合、またはあなたが作っているものを共有したい場合は、こちらをクリックしてください。

最新の開発者向けニュース、ヒント、イベント情報をお届けします。

シェア:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeシニアPHPデベロッパー

スタンダップ・コメディーの学位論文を持つ俳優の訓練を受け、ミートアップ・シーンを経てPHP開発に携わるようになった。技術について話したり書いたり、レコード・コレクションから変わったレコードを再生したり買ったりしています。