How to create and read webhooks with PHP, Koyeb, and Bruno

Create and read your webhooks using PHP and Leaf PHP, test them using Bruno, and publish them using Koyeb. Here’s the full guide.

How to create and read webhooks with PHP, Koyeb, and Bruno

Being able to create and read webhooks is critical, as it ensures that our applications won’t waste time on unnecessary server requests. With the use of PHP, LeafPHP, Koyeb, and Bruno, we will delve into the exploration of webhooks.

What are we going to talk about?

What are webhooks?

Webhooks are notifications triggered by specific events, such as receiving an email, opening a link within an email, creating or deleting an event, and more. They are crucial as they automatically inform our applications of significant occurrences without the necessity for periodic information retrieval at set intervals.

Why are webhooks important?

A webhook is initiated by the server and sent to your application without the need for your application to explicitly request it. Instead of making multiple requests, your application can simply wait until it receives a webhook. This not only enhances the efficiency of applications but, more importantly, accelerates their processing speed.

In scenarios like sending an email or creating an event, it becomes crucial to ascertain whether the message was opened, if there was a click within the messages, or if an event was modified or deleted. Having access to this information is instrumental in making informed decisions.

Google Pub/Sub for message sync

We can use Google Pub/Sub to sync Gmail messages between Google and Nylas in real-time. While this is not mandatory, is highly recommended.

Here are the steps as detailed in our documentation.

Google Pub/Sub

Which tools are we going to use?

As indicated in the title, we will utilize a distinct set of tools to create and read webhooks like PHP, Koyeb and Bruno:

  • PHP → A popular general-purpose scripting language that is especially suited to web development.
  • Koyeb → Deploy to production, scale globally, in minutes.
  • Bruno → Fast and Git-Friendly Opensource API client, aimed at revolutionizing the status quo represented by Postman, Insomnia and similar tools out there.

Creating a PHP reading webhooks application

Contrary to what you might believe, before creating a webhook, it’s essential to have the ability to read it. This might seem counterintuitive, but it makes sense in practice. When we initiate the creation of a webhook, Nylas needs to verify the existence of a valid account and the validity of the creation request. Without this verification, anyone could create webhooks indiscriminately.

This is why the first step is to create the reading application.

First, we will install Composer, a package manager for PHP, similar to npm, pip or gem.


To install it we need first to download the installer rename it to composer-setup.php and then run it on the terminal window:

$ php composer-setup.php --install-dir=~/.local/bin --filename=composer

Now we can call it from the terminal window:

$ composer

Once that’s installed, we’re going to create a folder named php-read-webhooks. Open your terminal window and navigate to that folder.

While plain PHP is sufficient for creating web applications, there are instances where a framework is beneficial for achieving better organization and structure in the code. For this specific project, I have opted for LeafPHP, a straightforward and elegant PHP micro-framework.

Leaf PHP

We can install it with Composer, using the following command:

$ composer require leafs/leaf

As we’ll require a frontend template system, we’re going to use Blade, a Lavarel template engine.

$ composer require leafs/blade

Now, something unexpected arises – a twist you might not have anticipated. I certainly didn’t. Here’s how the story unfolds:

I intended to utilize sessions to store each webhook call, avoiding reliance on any database since PHP lacks Data Classes. Using Globals was ruled out. During testing, everything appeared fine until I observed that the GET section generated one session, while the POST section created another. This posed a significant issue, as two distinct sessions couldn’t communicate. I attempted to use cookies to preserve and reuse the session ID value but encountered the problem of generating two different cookies.

To resolve this, I sought a solution to store the session ID in a plain file for easy retrieval. Simple-TxtDb came to the rescue – a single class capable of writing and reading plain files with zero dependencies. I submitted a pull request (which was merged 🥳) to address an issue related to dynamic properties.

One small change is to change this line on the txtdb.class.php file:

private $_db_dir = __DIR__ . '/db/';

For this one:

private $_db_dir = __DIR__ . '/database/';

Thinking back, the browser is one session and the webhook coming from Nylas is another, so it’s obvious that they will be different. Also, using sessions makes things a bit cumbersome, so let’s just use the plain text database and be done with it.

Now, let’s proceed to create our index.php file, which will manage all the webhook-related tasks.

// Load dependencies
require __DIR__ . '/vendor/autoload.php';
use Leaf\Blade;

// Declare global objects
$app = new Leaf\App();
$blade = new Blade('views', 'storage/cache');
$db = new TxtDb();

// Class to hold Webhooks information
class Webhook
	public $id;
	public $date;
	public $subject;
	public $from_email;
	public $from_name;

// Main page
$app->get('/', function() use($app, $blade, $db){
  // Check our text db for a recorded session
  $webhooks =  $db->select('webhooks');
  // Display it
  echo $blade->render('webhooks', ['webhooks' => $webhooks]);

// This will be called to validate our webhook
$app->get('/webhook', function () use($app) {
  $challenge = request()->get('challenge');
  //Return the challenge
  echo "$challenge";

// Page for the Webhook to send the information to
$app->post('/webhook', function () use($app, $db) {
  // Read the webhook information
  $json = file_get_contents('php://input', true);
  // Decode the json
  $data = json_decode($json);
  $is_genuine = verify_signature(file_get_contents('php://input'),
                                 mb_convert_encoding(getenv('CLIENT_SECRET'), 'UTF-8', 'ISO-8859-1'),
  # Is it really coming from Nylas? 
    response()->status(401)->plain('Signature verification failed!');
  error_log("Time to save the webhook");

  $webhook = new Webhook();
  // Fetch all the webhook information
  $webhook->id = $data->data->object->id;
  $date = $data->data->object->date;
  $date = new DateTime("@$date");
  $date = $date->format('Y-m-d H:i:s'); 
  $webhook->date = $date;
  $webhook->subject = $data->data->object->subject;
  $webhook->from_email = $data->data->object->from[0]->email;
  $webhook->from_name = $data->data->object->from[0]->name;
  // Store the webhook information into the session
  $db->insert("webhooks", ["webhook" => $webhook]); 
  error_log("Webhook was saved");
  // Return success back to Nylas
  response()->status(200)->plain('Webhook received');

// Function to verify the signature
function verify_signature($message, $key, $signature){
  $digest = hash_hmac('sha256', $message, $key);
  return(hash_equals($digest, $signature));

// Run the app

We also need to create a folder named views and inside a file named webhooks.blade.php:

<!Doctype html>
        <script src=""></script>     
   <h1 class="text-4xl font-bold dark:text-black bg-green-600 border-green-600 border-b p-4 m-4 rounded grid place-items-center">Webhooks</h1>
   <table style="width:100%">
       <tr class="bg-green-600 border-green-600 border-b p-4 m-4 rounded">
           <th>From Email</th>
           <th>From Name</th>
        @if (!is_null($webhooks))
          @foreach ($webhooks as $webhook["webhook"])
            @foreach ($webhook["webhook"] as $webhook_elem)
                <tr class="bg-white-600 border-green-600 border-b p-4 m-4 rounded">
                  <td><p class="text-sm font-semibold">{{$webhook_elem["id"]}}</p></td>
                  <td><p class="text-sm font-semibold">{{$webhook_elem["date"]}}</p></td>
                  <td><p class="text-sm font-semibold">{{$webhook_elem["subject"]}}</p></td>
                  <td><p class="text-sm font-semibold">{{$webhook_elem["from_email"]}}</p></td>
                  <td><p class="text-sm font-semibold">{{$webhook_elem["from_name"]}}</p></td>

To ensure that GitHub creates the necessary folders when we upload our project, let’s create a new folder with a dummy file inside. Create a folder named database, containing a file named “dummy” with a simple text like “DO NOT DELETE.”.

The folder structure should look like this:

Folder Structure

BTW, storage will be generated when we run the project locally.

Running the reading webhooks application locally

Now that we have all the source code in place, it’s time to run our application. Simply, run the following command on the terminal window:

$ php -S localhost:8000

We can now, open our favourite browser:

Webhooks Main Page

Testing the reading webhooks application locally with Bruno

Initially, we must make our application accessible to the outside world, but we don’t intend to host it anywhere at the moment since we are still in the testing phase.

Keep in mind that as we’re going to test locally, we’re not going to authenticate that the webhook is coming from Nylas, so we need to comment out lines 43 to 49.

PHP Webhooks source code

We’re going to use Bruno, an amazing tool to test APIs. It stores everything on your filesystem and the majority of features are free and open source. PHP and Bruno work perfectly together. Later, adding LeafPHP and Koyeb will make our webhooks application a great tool.


We can install it with Homebrew:

$ brew install bruno

When we launch it, we need to create a collection, so we can create one and name it Nylas-V3.

After this, we can add calls, in our case http://localhost:8000/webhook

Bruno API Tool

On the Body tab, we’re going to use this Webhook response template:

    "specversion": "1.0",
    "type": "message.created",
    "source": "/google/emails/realtime",
    "id": "xxx",
    "time": 1709379109,
    "webhook_delivery_attempt": 1,
    "data": {
        "application_id": "xxx",
        "object": {
            "body": "Email Body",
            "created_at": 1709379093,
            "date": 1709379093,
            "folders": [
            "from": [
                    "email": "",
                    "name": "Alvaro Tejada Galindo"
            "grant_id": "xxx",
            "id": "xxx",
            "object": "message",
            "snippet": "Yup!",
            "starred": false,
            "subject": "Hey! Ho!",
            "thread_id": "18dfeef3637c1813",
            "to": [
                    "email": "xxx",
                    "name": "Blag aka Alvaro Tejada Galindo"
            "unread": true
PHP Response in Bruno

With our application running, all that’s required is to press Enter and let Bruno do its job.

Webhook received

Upon opening our reader application, we will observe the successful recording of the webhook:

Webhook posted

Awesome, now that we know it’s working, we can uncomment lines 43 to 49.

Deploying the reading webhooks application with Koyeb

In the past, Heroku would have been a suitable choice, however, its free tier is no longer available. Therefore, it’s time to explore better alternatives.

One such alternative is Koyeb, which is completely free to up to one service. The next one will cost $0.0022/hr on the Eco plan.


Firstly, we should upload our source code to GitHub, place it in a project named php-read-webhooks (or your chosen name), and include two essential files: Procfile and .htaccess.

This is Procfile:

web: heroku-php-apache2

And this is .htaccess:

<IfModule mod_rewrite.c>
  Options +FollowSymlinks
  Options +Indexes
  RewriteEngine on
  # if your app is in a subfolder
  # RewriteBase /my_app/ 

  # test string is a valid files
  RewriteCond %{SCRIPT_FILENAME} !-f
  # test string is a valid directory
  RewriteCond %{SCRIPT_FILENAME} !-d

  RewriteRule ^(.*)$   index.php?uri=/$1    [NC,L,QSA]
  # with QSA flag (query string append),
  # forces the rewrite engine to append a query string part of the
  # substitution string to the existing string, instead of replacing it.

Your folder structure should look like this:

Github Folder Structure

Now, we need to move into Koyeb. Upon logging in, we will encounter this screen:

Koyeb configuration

We’re going to create one environmental variable. Port comes by default, so we need to add the CLIENT_SECRET.

Define client secret in Koyeb

We need to press Deploy once we finished. Koyeb will start deploying our application, and this might take a couple of minutes.

Create application

Once ready, we will deploy our application and access it.

PHP Koyeb application ready to go

Creating the create webhooks application

Now that our Read Webhooks application is up and running, it’s time to create the Webhooks application. We going to create a folder named php-create-folder. Run the following Composer command:

$ composer require vlucas/phpdotenv

This command will install phpdotenv, which will help us use .env files in our application.

If we had previously executed this command in a higher-level folder, it would prompt us to use it as a template. We should decline, as we aim to generate a new one. This process will generate the files composer.json, composer.lock, and the vendor folder

Now, we can proceed by creating an “index.php” file:

// Import your dependencies

// Load env variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);

// Define the headers
$headr = array();
$headr[] = 'Accept: application/json';
$headr[] = 'Content-type: application/json';
$headr[] = 'Authorization: Bearer ' . $_ENV['V3_TOKEN'];

// Array with information needed to create the webhook
$data = array(
            'description' => "My PHP Webhook",
            'trigger_types' => array("message.created"),
            'webhook_url' => "",
            'notification_email_addresses' => array($_ENV['EMAIL'])

// Call the webhooks endpoint
$ch = curl_init( "" );
// Encode the data as JSON
$payload = json_encode( $data );
// Submit the Email information
curl_setopt( $ch, CURLOPT_POSTFIELDS, $payload );
// Submit the Headers
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headr);
// We're doing a POST
curl_setopt($ch, CURLOPT_POST, true);
// Return response instead of printing.
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
// Send request.
$result = curl_exec($ch);
echo $result;
// Close request

We must create an .env file to store our credentials:


Running the create webhooks application

Run these commands on the terminal window to execute the application:

$ php index.php
Running the PHP create webhooks application

Now, copy the webhookSecret value and update the Client Secret environment variable.

Our application will display each new event created.

PHP Webhooks page with some webhooks

What’s next?

Utilizing PHP, LeafPHP, Koyeb and Bruno to create and read webhooks was a great experience.

Do you have any comments or feedback? Please use our forums 😎

Here’s the repo for Read PHP Webhooks and Create PHP Webhooks.

Don’t miss the action, join our LiveStream Coding with Nylas!

You May Also Like

Send emails using TinyMCE Rich Text Editor
How to create a scheduler with Python and Taipy
hero banner
How to build an email responder with generative AI

Subscribe for our updates

Please enter your email address and receive the latest updates.