How to create and read Google Webhooks using Kotlin

15 min read

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 Kotlin, we will delve into the exploration of webhooks. We’re going to learn how to use Kotlin to read and create Google 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?

The server initiates a webhook and sends it to your application without requiring 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 recipient opened the message, clicked within the messages, or modified or deleted an event. Having access to this information is instrumental in making informed decisions.

We’re going to create Google Webhooks to interact with our Google Calendar and the Kotlin programming language.

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.

Setup Google Pub/Sub

Creating a 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.

We’ll begin by establishing a new Kotlin project, which we’ll name kotlin-webhooks.

This will be our pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://maven.apache.org/POM/4.0.0"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <artifactId>kotlin-read-webhooks</artifactId>
   <groupId>org.example</groupId>
   <version>1.0-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>consoleApp</name>

   <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <kotlin.code.style>official</kotlin.code.style>
       <kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
       <main.class>WebhooksKt</main.class>
   </properties>

   <repositories>
       <repository>
           <id>mavenCentral</id>
           <url>https://repo1.maven.org/maven2/</url>
       </repository>
   </repositories>

   <build>
       <sourceDirectory>src/main/kotlin</sourceDirectory>
       <testSourceDirectory>src/test/kotlin</testSourceDirectory>
       <plugins>
           <plugin>
               <groupId>org.jetbrains.kotlin</groupId>
               <artifactId>kotlin-maven-plugin</artifactId>
               <version>1.9.0</version>
               <executions>
                   <execution>
                       <id>compile</id>
                       <phase>compile</phase>
                       <goals>
                           <goal>compile</goal>
                       </goals>
                   </execution>
                   <execution>
                       <id>test-compile</id>
                       <phase>test-compile</phase>
                       <goals>
                           <goal>test-compile</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
           <plugin>
               <artifactId>maven-surefire-plugin</artifactId>
               <version>2.22.2</version>
           </plugin>
           <plugin>
               <artifactId>maven-failsafe-plugin</artifactId>
               <version>2.22.2</version>
           </plugin>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <configuration>
                   <source>8</source>
                   <target>8</target>
               </configuration>
           </plugin>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-assembly-plugin</artifactId>
               <version>2.6</version>
               <executions>
                   <execution>
                       <id>make-assembly</id>
                       <phase>package</phase>
                       <goals> <goal>single</goal> </goals>
                       <configuration>
                           <archive>
                               <manifest>
                                   <mainClass>${main.class}</mainClass>
                               </manifest>
                           </archive>
                           <descriptorRefs>
                               <descriptorRef>jar-with-dependencies</descriptorRef>
                           </descriptorRefs>
                       </configuration>
                   </execution>
               </executions>
           </plugin>
       </plugins>
   </build>

   <dependencies>
       <dependency>
           <groupId>org.jetbrains.kotlin</groupId>
           <artifactId>kotlin-test-junit5</artifactId>
           <version>1.9.0</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.junit.jupiter</groupId>
           <artifactId>junit-jupiter-engine</artifactId>
           <version>5.9.2</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.jetbrains.kotlin</groupId>
           <artifactId>kotlin-stdlib-jdk8</artifactId>
           <version>1.9.0</version>
       </dependency>
       <dependency>
           <groupId>com.nylas.sdk</groupId>
           <artifactId>nylas</artifactId>
           <version>2.0.0</version>
       </dependency>
       <dependency>
           <groupId>io.github.cdimascio</groupId>
           <artifactId>dotenv-kotlin</artifactId>
           <version>6.4.1</version>
       </dependency>
       <dependency>
           <groupId>com.sparkjava</groupId>
           <artifactId>spark-kotlin</artifactId>
           <version>1.0.0-alpha</version>
       </dependency>
       <dependency>
           <groupId>com.sparkjava</groupId>
           <artifactId>spark-template-mustache</artifactId>
           <version>2.7.1</version>
       </dependency>
       <dependency>
           <groupId>com.github.spullara.mustache.java</groupId>
           <artifactId>compiler</artifactId>
           <version>0.9.4</version>
       </dependency>
       <dependency>
           <groupId>com.fasterxml.jackson.module</groupId>
           <artifactId>jackson-module-kotlin</artifactId>
           <version>2.14.2</version>
       </dependency>
       <dependency>
           <groupId>io.github.cdimascio</groupId>
           <artifactId>dotenv-java</artifactId>
           <version>2.3.2</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-api</artifactId>
           <version>2.0.7</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-simple</artifactId>
           <version>2.0.7</version>
       </dependency>
   </dependencies>

</project>

Let’s create our main class called Webhooks.kt:

// Import Nylas packages
import com.nylas.NylasClient
import com.nylas.models.FindEventQueryParams

// Import Spark and Jackson packages
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import spark.template.mustache.MustacheTemplateEngine;
import com.nylas.models.When

import spark.ModelAndView
import spark.kotlin.Http
import spark.kotlin.ignite

import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.net.URLEncoder
import java.text.SimpleDateFormat

data class Webhook_Info(
    var id: String,
    var date: String,
    var title: String,
    var description: String,
    var participants: String,
    var status: String
)

var array: Array<Webhook_Info> = arrayOf()

object Hmac {
    fun digest(
        msg: String,
        key: String,
        alg: String = "HmacSHA256"
    ): String {
        val signingKey = SecretKeySpec(key.toByteArray(), alg)
        val mac = Mac.getInstance(alg)
        mac.init(signingKey)

        val bytes = mac.doFinal(msg.toByteArray())
        return format(bytes)
    }

    private fun format(bytes: ByteArray): String {
        val formatter = Formatter()
        bytes.forEach { formatter.format("%02x", it) }
        return formatter.toString()
    }
}

fun addElement(arr: Array<Webhook_Info>, element: Webhook_Info): Array<Webhook_Info> {
    val mutableArray = arr.toMutableList()
    mutableArray.add(element)
    return mutableArray.toTypedArray()
}

fun dateFormatter(milliseconds: String): String {
    return SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(Date(milliseconds.toLong() * 1000)).toString()
}

fun main(args: Array<String>) {
    val http: Http = ignite()
    // Initialize Nylas client
    val nylas: NylasClient = NylasClient(
        apiKey = System.getenv("V3_TOKEN")
    )

    http.get("/webhook") {
        request.queryParams("challenge")
    }

    http.post("/webhook") {
        print("Hello from Webhook")
        val mapper = jacksonObjectMapper()
        val model: JsonNode = mapper.readValue<JsonNode>(request.body())
        if(model["data"]["object"]["calendar_id"].textValue().equals(System.getenv("CALENDAR_ID"), false)){
            if(Hmac.digest(request.body(), URLEncoder.encode(System.getenv("CLIENT_SECRET"), "UTF-8")) == 
                request.headers("X-Nylas-Signature").toString()){
                val eventquery = FindEventQueryParams(System.getenv("CALENDAR_ID"))
                System.out.println(model["data"]["object"]["id"].textValue())
                val myevent = nylas.events().find(System.getenv("GRANT_ID"), eventId = model["data"]
                                                 ["object"]["id"].textValue(), queryParams = eventquery)
                var participants: String = ""
                for (participant in myevent.data.participants){
                    participants = "$participants;${participant.email.toString()}"
                }
                var event_datetime: String = ""
                when(myevent.data.getWhen().getObject().toString()) {
                    "DATESPAN" -> {
                        val datespan = myevent.data.getWhen() as When.Datespan
                        event_datetime = datespan.startDate.toString()
                    }
                    "TIMESPAN" -> {
                        val timespan = myevent.data.getWhen() as When.Timespan
                        val startDate = dateFormatter(timespan.startTime.toString())
                        val endDate = dateFormatter(timespan.endTime.toString())
                        event_datetime = "From $startDate to $endDate"
                    }
                    "DATE" -> {
                        val datespan = myevent.data.getWhen() as When.Date
                        event_datetime = datespan.date
                    }
                }
                participants = participants.drop(1)
                array = addElement(array, Webhook_Info(myevent.data.id, event_datetime.toString(), 
                                   myevent.data.title.toString(), myevent.data.description.toString(), 
                                   participants, myevent.data.status.toString()))
            }
        }
        ""
    }

    http.get("/") {
        val model = HashMap<String, Any>()
        model["webhooks"] = array
        MustacheTemplateEngine().render(
            ModelAndView(model, "show_webhooks.mustache")
        )
    }
}

We need a template to display the webhooks. Create a templates folder inside the resources folder, then create a new file and call it show_ids.mustache:

<html>
<head>
   <script src="https://cdn.tailwindcss.com"></script>
   <title>Webhooks</title>
</head>
<body>
<div class="bg-green-600 border-green-600 border-b p-4 m-4 rounded w-2/5 grid place-items-center">
   <br>
   <table style="width:100%">
       <tr>
           <th>Id</th>
           <th>Date</th>
           <th>Title</th>
           <th>Description</th>
           <th>Participants</th>
           <th>Status</th>
       </tr>
   {{#webhooks}}
       <tr>
           <td><p class="font-semibold">{{id}}</p></td>
           <td><p class="font-semibold">{{date}}</p></td>
           <td><p class="font-semibold">{{title}}</p></td>
           <td><p class="font-semibold">{{description}}</p></td>
           <td><p class="font-semibold">{{participants}}</p></td>
           <td><p class="font-semibold">{{status}}</p></td>
       </tr>
   {{/webhooks}}
   </table>
</div>
</body>
</html>

Everything is prepared, but the application must be deployed to be accessible from the outside.

Deploying the Reading Webhooks Application

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 Qoddi, which requires a card for verifying a newly created account—this can be either a debit or credit card.

Firstly, we should upload our source code to GitHub, place it in a project named kotlin-web-hooks (or your chosen name), and include two essential files: Procfile and system.properties.

This is Procfile:

web: java -jar /workspace/target/kotlin-read-webhooks-1.0-SNAPSHOT-jar-with-dependencies.jar

And this is system.properties:

java.runtime.version=11
maven.version=3.9.4

Your folder structure should look like this:

Kotlin project structure

Now, we need to move into Qoddi. When we log in, we will be presented with this screen:

We can use Java to create the Kotlin project

If we select any of the tiles, we will be redirected to the documentation for the corresponding programming language or environment. We need to press New to create a new project.

We’re going to name it kotlin-read-webhooks, we don’t need a datastore, its type is going to be web server and the app size XS:

Choosing the Webhooks Kotlin Repository

Upon clicking on the Next button, we need to choose Github, as we’re going to upload our code from there:

Build options

We need to allow Qoddi to access our repository, so we need to make sure that it’s public:

We need to select our repository and then simply continue with the next screen:

Once we make sure that everything is correct, we need to press the Launch My App button:

Launching the Google Webhooks with Kotlin application

Qoddi will start deploying our application, and this might take a couple of minutes:

Google Webhooks Kotlin Project

We need to click on Activate SSL so that our application gets accessed as an https application:

Also, we’re not going to use .env files here, so we need to create Environment Variables:

Port and Host comes by default, so we need to add the rest:

Leave CLIENT_SECRET empty (or set it to another value) for now, as we will update it later.

One detail that can be easily overlooked is specifying the Container Port, which, for a Spark application is 4567. Additionally, ensure that Redirect Http to Https is activated:

Adding Port and SSL

After launching the application, grab the URL and open it in a web browser. It will be empty, as we haven’t defined any webhooks yet:

Creating the Create Webhooks Application

Now that our Read Webhooks application is up and running, it’s time to create the Create Webhooks application.

Let’s create a project called kotlin-create-webhooks, with the following pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://maven.apache.org/POM/4.0.0"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <artifactId>kotlin-create-webhooks</artifactId>
   <groupId>org.example</groupId>
   <version>1.0-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>consoleApp</name>

   <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <kotlin.code.style>official</kotlin.code.style>
       <kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
       <main.class>CreateWebhooksKt</main.class>
   </properties>

   <repositories>
       <repository>
           <id>mavenCentral</id>
           <url>https://repo1.maven.org/maven2/</url>
       </repository>
   </repositories>

   <build>
       <sourceDirectory>src/main/kotlin</sourceDirectory>
       <testSourceDirectory>src/test/kotlin</testSourceDirectory>
       <plugins>
           <plugin>
               <groupId>org.jetbrains.kotlin</groupId>
               <artifactId>kotlin-maven-plugin</artifactId>
               <version>1.9.0</version>
               <executions>
                   <execution>
                       <id>compile</id>
                       <phase>compile</phase>
                       <goals>
                           <goal>compile</goal>
                       </goals>
                   </execution>
                   <execution>
                       <id>test-compile</id>
                       <phase>test-compile</phase>
                       <goals>
                           <goal>test-compile</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
           <plugin>
               <artifactId>maven-surefire-plugin</artifactId>
               <version>2.22.2</version>
           </plugin>
           <plugin>
               <artifactId>maven-failsafe-plugin</artifactId>
               <version>2.22.2</version>
           </plugin>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <configuration>
                   <source>8</source>
                   <target>8</target>
               </configuration>
           </plugin>
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-assembly-plugin</artifactId>
               <version>2.6</version>
               <executions>
                   <execution>
                       <id>make-assembly</id>
                       <phase>package</phase>
                       <goals> <goal>single</goal> </goals>
                       <configuration>
                           <archive>
                               <manifest>
                                   <mainClass>${main.class}</mainClass>
                               </manifest>
                           </archive>
                           <descriptorRefs>
                               <descriptorRef>jar-with-dependencies</descriptorRef>
                           </descriptorRefs>
                       </configuration>
                   </execution>
               </executions>
           </plugin>
       </plugins>
   </build>

   <dependencies>
       <dependency>
           <groupId>org.jetbrains.kotlin</groupId>
           <artifactId>kotlin-test-junit5</artifactId>
           <version>1.9.0</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.junit.jupiter</groupId>
           <artifactId>junit-jupiter-engine</artifactId>
           <version>5.9.2</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.jetbrains.kotlin</groupId>
           <artifactId>kotlin-stdlib-jdk8</artifactId>
           <version>1.9.0</version>
       </dependency>
       <dependency>
           <groupId>com.nylas.sdk</groupId>
           <artifactId>nylas</artifactId>
           <version>2.0.0-beta.1</version>
       </dependency>
       <dependency>
           <groupId>io.github.cdimascio</groupId>
           <artifactId>dotenv-kotlin</artifactId>
           <version>6.4.1</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-api</artifactId>
           <version>2.0.7</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-simple</artifactId>
           <version>2.0.7</version>
       </dependency>
   </dependencies>

</project>

It’s important that we create an .env file to store our credentials:

V3_TOKEN=<YOUR_V3_TOKEN>
GRANT_ID=<YOUR_GRANT_ID>
CALENDAR_ID=<YOUR_CALENDAR_ID>

The application will call the main file CreateWebhooks.kt:

// Import Nylas packages
import com.nylas.NylasClient
import com.nylas.models.CreateWebhookRequest
import com.nylas.models.Response
import com.nylas.models.Webhook
import com.nylas.models.WebhookTriggers
import com.nylas.resources.Webhooks

// Import DotEnv to handle .env files
import io.github.cdimascio.dotenv.dotenv

// The main function
fun main(args: Array<String>){
   // Load our env variable
   val dotenv = dotenv()

   // Initialize Nylas client
   val nylas: NylasClient = NylasClient(
       apiKey = dotenv["V3_TOKEN"]
   )

   // Which triggers are we're going to use
   val triggersList: List<WebhookTriggers> = listOf(WebhookTriggers.EVENT_CREATED)
   // Create the webhook triggers request
   val webhookRequest: CreateWebhookRequest = CreateWebhookRequest(triggersList, "
   <YOUR_WEBHOOK_URL>/webhooks", "My first webhook",dotenv["GRANT_ID"])
   // Create the webhook and return the response
   val webhook: Response<WebhookWithSecret> = 
                         Webhooks(nylas).create(webhookRequest)
   // Print the webhook information
   println(webhook.data)
}

Running the Create Webhooks Application

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

$ mvn package
$ java -jar target/kotlin-create-webhooks-1.0-SNAPSHOT-jar-with-dependencies.jar
Creating a Google Webhook using Kotlin

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

Our application will be able to display each new event created.

Reading Google Calendar Webhooks using Kotlin

What’s next?

Using Google Webhooks with Kotlin is easy and powerful. Let’s see what can you create! 🥳

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

Want to fetch the repo? Here you go kotlin-read-webhooks 🥳

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

Related resources

How to block time slots in Outlook and Google calendar with Nylas Calendar API

Key Takeaways Managing calendar availability is essential for professionals, teams, and businesses to stay organized…

How to Solve Webhook Integration Challenges with PubSub Notification Channel

Key Takeaways This article addresses the challenges of webhook integration and introduces the PubSub Notification…

How to Send Emails Using an API

Key Takeaways This post will provide a complete walkthrough for integrating an email API focused…