Webhook Quickstart: create and read webhooks
This guide walks you through the process of creating a sample web app that receives webhooks from Nylas whenever you get a new email.
✨ The complete code for this Quickstart guide is available on GitHub. You can clone the repo and run the app to see it in action for the Nylas SDK language of your choice: Node.js, Python, Ruby, Java, or Kotlin.
Before you begin
Make sure you have done the following
- Create a Nylas account- Nylas offers a free Sandbox account where you can test APIs and set up webhook triggers. Sign up to create a Nylas account.
- Made an endpoint accessible to the web, so that Nylas can make a request to your webhook. If you're on localhost you should use a tunneling service, we recommend using the VS Code tunnel. Keep in mind that Ngrok may rate limit you and should be avoided if possible.
Set up webhooks in your app
First, we'll set up the foundation for creating and receiving webhooks in your web app.
⚠️ Remember that you will need to upload this source code to a hosting server to make your web app accessible. You'll also need to define the WEBHOOK_SECRET
variable, which is a value Nylas provides when you create your webhook.
Configure Nylas SDKs and webhook endpoint
import "dotenv/config";
import express from "express";
import Nylas from "nylas";
import crypto from 'crypto';
const app = express();
const port = 3000;
// Route to respond to Nylas webhook creation with challenge parameter.
app.get("/webhooks/nylas", (req, res) => {
// This occurs when you first set up the webhook with Nylas
if (req.query.challenge) {
console.log(`Received challenge code! - ${req.query.challenge}`);
// Enable the webhook by responding with the challenge parameter.
return res.send(req.query.challenge);
}
console.log(JSON.stringify(req.body.data));
return res.status(200).end();
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
# Import packages
from flask import Flask, request, render_template
import hmac
import hashlib
import os
from dataclasses import dataclass
import pendulum
# Array to hold webhook dataclass
webhooks = []
# Webhook dataclass
@dataclass
class Webhook:
_id: str
date: str
subject: str
from_email: str
from_name: str
# Get today’s date
today = pendulum.now()
# Create the Flask app and load the configuration
app = Flask(__name__)
# Read and insert webhook data
@app.route("/webhook", methods=["GET", "POST"])
def webhook():
# We are connected to Nylas, let’s return the challenge parameter.
if request.method == "GET" and "challenge" in request.args:
print(" * Nylas connected to the webhook!")
return request.args["challenge"]
if request.method == "POST":
is_genuine = verify_signature(
message=request.data,
key=os.environ["WEBHOOK_SECRET"].encode("utf8"),
signature=request.headers.get("X-Nylas-Signature"),
)
if not is_genuine:
return "Signature verification failed!", 401
data = request.get_json()
hook = Webhook(
data["data"]["object"]["id"],
pendulum.from_timestamp(
data["data"]["object"]["date"], today.timezone.name
).strftime("%d/%m/%Y %H:%M:%S"),
data["data"]["object"]["subject"],
data["data"]["object"]["from"][0]["email"],
data["data"]["object"]["from"][0]["name"],
)
webhooks.append(hook)
return "Webhook received", 200
# Main page
@app.route("/")
def index():
return render_template("main.html", webhooks=webhooks)
# Signature verification
def verify_signature(message, key, signature):
digest = hmac.new(key, msg=message, digestmod=hashlib.sha256).hexdigest()
return hmac.compare_digest(digest, signature)
# Run our application
if __name__ == "__main__":
app.run()
# frozen_string_literal: true
# Load gems
require 'nylas'
require 'sinatra'
require 'sinatra/config_file'
webhook = Data.define(:id, :date, :subject, :from_email, :from_name)
webhooks = []
get '/webhook' do
params['challenge'].to_s if params.include? 'challenge'
end
post '/webhook' do
# We need to verify that the signature comes from Nylas
is_genuine = verify_signature(request.body.read, ENV['WEBHOOK_SECRET'],
request.env['HTTP_X_NYLAS_SIGNATURE'])
unless is_genuine
status 401
'Signature verification failed!'
end
# Read the webhook information and store it on the data class.
request.body.rewind
model = JSON.parse(request.body.read)
puts(model["data"]["object"])
hook = webhook.new(model["data"]["object"]["id"],
Time.at(model["data"]["object"]["date"]).strftime("%d/%m/%Y %H:%M:%S"),
model["data"]["object"]["subject"], model["data"]["object"]["from"][0]["email"],
model["data"]["object"]["from"][0]["name"])
webhooks.append(hook)
status 200
'Webhook received'
end
get '/' do
puts webhooks
erb :main, locals: { webhooks: webhooks }
end
# Generate a signature with our client secret and compare it with the one from Nylas.
def verify_signature(message, key, signature)
digest = OpenSSL::Digest.new('sha256')
digest = OpenSSL::HMAC.hexdigest(digest, key, message)
secure_compare(digest, signature)
end
# Compare the keys to see if they are the same
def secure_compare(a_key, b_key)
return false if a_key.empty? || b_key.empty? || a_key.bytesize != b_key.bytesize
l = a_key.unpack "C#{a_key.bytesize}"
res = 0
b_key.each_byte { |byte| res |= byte ^ l.shift }
res.zero?
end
//webhook_info.java
import lombok.Data;
@Data
public class Webhook_Info {
private String id;
private String date;
private String subject;
private String from_email;
private String from_name;
}
// Import Spark, Jackson and Mustache libraries
import spark.ModelAndView;
import static spark.Spark.*;
import spark.template.mustache.MustacheTemplateEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
// Import Java libraries
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
// Import external libraries
import org.apache.commons.codec.digest.HmacUtils;
public class ReadWebhooks {
// Function to get Hmac
public static String getHmac(String data, String key) {
return new HmacUtils("HmacSHA256", key).hmacHex(data);
}
public static void main(String[] args) {
// Array list of Webhooks
ArrayList<Webhook_Info> array = new ArrayList<Webhook_Info>();
// Default path when we load our web application
get("/", (request, response) -> {
// Create a model to pass information to the mustache template
Map<String, Object> model = new HashMap<>();
model.put("webhooks", array);
// Call the mustache template
return new ModelAndView(model, "show_webhooks.mustache");
}, new MustacheTemplateEngine());
// Validate our webhook with the Nylas server
get("/webhook", (request, response) -> request.queryParams("challenge"));
// Get webhook information
post("/webhook", (request, response) -> {
// Create JSON object mapper
ObjectMapper mapper = new ObjectMapper();
// Read the response body as a Json object
JsonNode incoming_webhook = mapper.readValue(request.body(), JsonNode.class);
// Make sure we're reading our calendar
if (getHmac(request.body(), URLEncoder.
encode(System.getenv("WEBHOOK_SECRET"), "UTF-8")).
equals(request.headers("X-Nylas-Signature"))) {
// Create Webhook_Info record
Webhook_Info new_webhook = new Webhook_Info();
// Fill webhook information
System.out.println(incoming_webhook.get("data").get("object"));
new_webhook.setId(incoming_webhook.get("data").
get("object").get("id").textValue());
new_webhook.setDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").
format(new java.util.Date((incoming_webhook.get("data").
get("object").get("date").asLong() * 1000L))));
new_webhook.setSubject(incoming_webhook.get("data").
get("object").get("subject").textValue());
new_webhook.setFrom_email(incoming_webhook.get("data").
get("object").get("from").get(0).get("email").textValue());
new_webhook.setFrom_name(incoming_webhook.get("data").
get("object").get("from").get(0).get("name").textValue());
// Add webhook call to an array, so that we display it on screen
array.add(new_webhook);
}
response.status(200);
return "Webhook Received";
});
}
}
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 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 subject: String,
var fromEmail: String,
var fromName: 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()
http.get("/webhook") {
request.queryParams("challenge")
}
http.post("/webhook") {
val mapper = jacksonObjectMapper()
val model: JsonNode = mapper.readValue<JsonNode>(request.body())
if(Hmac.digest(request.body(), URLEncoder.encode(System.getenv("WEBHOOK_SECRET"),
"UTF-8")) == request.headers("X-Nylas-Signature").toString()){
array = addElement(array, Webhook_Info(model["data"]["object"]["id"].textValue(),
dateFormatter(model["data"]["object"]["id"].textValue()),
model["data"]["object"]["subject"].textValue(),
model["data"]["object"]["from"].get(0)["email"].textValue(),
model["data"]["object"]["from"].get(0)["name"].textValue()))
}
response.status(200)
"Webhook Received"
}
http.get("/") {
val model = HashMap<String, Any>()
model["webhooks"] = array
MustacheTemplateEngine().render(
ModelAndView(model, "show_webhooks.mustache")
)
}
}
Prepare to log webhook notifications
import "dotenv/config";
import express from "express";
import Nylas from "nylas";
import crypto from 'crypto';
const isValidWebhook (req) {
const signature = req.headers['x-nylas-signature'];
const digest = crypto
.createHmac(
'sha256',
process.env.WEBHOOK_SECRET // Comes from Nylas Dashboard. Make sure to set in your env vars
)
.update(req.body.data)
.digest('hex')
return digest === nylasSignature
}
const app = express();
const port = 3000;
// Route to receive webhook events after creating webhook
app.post("/webhooks/nylas", (req, res) => {
if (req.query.challenge) {
return res.send(req.query.challenge);
}
// Check to make sure it's actually Nylas that sent the webhook
if (!isValidWebhook(req)) {
return res.status(401).end
}
console.log(JSON.stringify(req.body.data));
// Responding to Nylas is important to prevent the webhook from retrying!
return res.status(200).end();
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
# Inside the templates folder
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
<title>Webhooks</title>
</head>
<body>
<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>Id</th>
<th>Date</th>
<th>Subject</th>
<th>From Email</th>
<th>From Name</th>
</tr>
{% for webhook in webhooks: %}
<tr class="bg-white-600 border-green-600 border-b p-4 m-4 rounded">
<td><p class="text-sm font-semibold">{{ webhook._id }}</p></td>
<td><p class="text-sm font-semibold">{{ webhook.date }}</p></td>
<td><p class="text-sm font-semibold">{{ webhook.subject }}</p></td>
<td><p class="text-sm font-semibold">{{ webhook.from_email }}</p></td>
<td><p class="text-sm font-semibold">{{ webhook.from_name }}</p></td>
</tr>
{% endfor %}
</table>
</body>
</html>
# Inside the views folder
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
<title>Webhooks</title>
</head>
<body>
<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>Id</th>
<th>Date</th>
<th>Subject</th>
<th>From Email</th>
<th>From Name</th>
</tr>
<% webhooks.each do |item| %>
<tr class="bg-white-600 border-green-600 border-b p-4 m-4 rounded">
<td><p class="text-sm font-semibold"><%= item.id %></p></td>
<td><p class="text-sm font-semibold"><%= item.date %></p></td>
<td><p class="text-sm font-semibold"><%= item.subject %></p></td>
<td><p class="text-sm font-semibold"><%= item.from_email %></p></td>
<td><p class="text-sm font-semibold"><%= item.from_name %></p></td>
</tr>
<% end %>
</table>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
<title>Webhooks</title>
</head>
<body>
<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>Id</th>
<th>Date</th>
<th>Subject</th>
<th>From Email</th>
<th>From Name</th>
</tr>
{{#webhooks}}
<tr class="bg-white-600 border-green-600 border-b p-4 m-4 rounded">
<td><p class="text-sm font-semibold">{{id}}</p></td>
<td><p class="text-sm font-semibold">{{date}}</p></td>
<td><p class="text-sm font-semibold">{{subject}}</p></td>
<td><p class="text-sm font-semibold">{{from_email}}</p></td>
<td><p class="text-sm font-semibold">{{from_name}}</p></td>
</tr>
{{/webhooks}}
</table>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdn.tailwindcss.com"></script>
<title>Webhooks</title>
</head>
<body>
<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>Id</th>
<th>Date</th>
<th>Subject</th>
<th>From Email</th>
<th>From Name</th>
</tr>
{{#webhooks}}
<tr class="bg-white-600 border-green-600 border-b p-4 m-4 rounded">
<td><p class="text-sm font-semibold">{{id}}</p></td>
<td><p class="text-sm font-semibold">{{date}}</p></td>
<td><p class="text-sm font-semibold">{{subject}}</p></td>
<td><p class="text-sm font-semibold">{{fromEmail}}</p></td>
<td><p class="text-sm font-semibold">{{fromName}}</p></td>
</tr>
{{/webhooks}}
</table>
</body>
</html>
Use webhooks with Nylas
Now that you have a webhook server set up, it's time to start sending email events by setting up your webhooks on the dashboard.
You can click on "Create Webhook" and input the call back url hosted on the server you just created and select message.created
trigger. Once created, the dashboard will display your webhook secret just once, that you need to store and pass as an environment variable under WEBHOOK_SECRET
.
If all of these steps went smoothly, you'll now get a webhook notification every time you send or receive an email!
Next steps
Congrats! 🎉 In this Quickstart guide, you set up a simple web app, created a webhook, and received webhook notifications using Nylas.
If you'd like to see the complete code for this guide, you can find it on GitHub for the Nylas SDK language of your choice—Node.js, Python, Ruby, Java, or Kotlin.
The links below are further reading as you continue your journey to Nylas greatness: