Integrating Nylas Notetaker into your meetings: a step-by-step guide for developers

8 min read

Introduction

In today’s fast-paced work, capturing meeting notes is challenging. Nylas Notetaker is a real-time meeting bot that joins your online meetings to record and transcribe.

In this guide, we’ll walk through how to integrate Nylas Notetaker into your meetings. Nylas Notetaker transcribes audio and records video from meetings, helping teams focus on the meeting’s objectives. Let’s go over how to get up and running step by step.

Prerequisites

Before you begin integrating Nylas Notetaker, make sure you have the following in place:

  • Nylas Developer Account: Sign up for a Nylas developer account for free if you haven’t already. You’ll need access to the Nylas Dashboard to retrieve API credentials.
  • Nylas API Key: From your Nylas dashboard, grab your API key.
  • Connected User Account (Grant ID): The grant ID represents the authorized account that Notetaker will use to join meetings. You can create a test grant from the Nylas developer dashboard.
  • Meeting Platform & Link: Have an online meeting link ready (e.g., a Google Meet, Microsoft Teams, or Zoom). Notetaker joins as a guest user, so someone might need to admit the bot into the meeting.
  • Necessary Permissions: Ensure your Nylas application has the right scopes/permissions. For example to get started with inviting Nylas Notetaker to a Google meet we just need the required Google scopes to authenticate a user to receive a user grant Id. For calendar integrations, we will need read access to calendars or events if you plan to fetch meeting details (like the join URL) via the Nylas API.

With these prerequisites met, you’re ready to start adding the Notetaker into your meetings.

Setting Up Nylas Notetaker

The first step in using Nylas Notetaker is authenticating with the Nylas API and preparing to invite the Notetaker bot. Here’s how to set everything up:

1. Configure API Authentication: In your development environment, set an environment variable for your Nylas API key (obtained from the dashboard) and note your grantId (the connected account’s grant ID).

# Set environment variables for reuse
NYLAS_API_KEY="<YOUR_NYLAS_API_KEY>"

When making API calls to Nylas, include the API key in the Authorization header. For example, a basic test to list all Notetaker bots scheduled for your account (if any) can be done with a GET notetakers request:

Node.js

Ruby

Python

Java

Curl

Kotlin

const Nylas = require("nylas");
const nylas = new Nylas({
  apiKey: "{NYLAS_API_KEY}",
});

async function listNotetakers() {
  const response = await nylas.notetakers.list({
    identifier: "{GRANT_ID}",
  });

  console.log("Notetakers:", response.data);
}

listNotetakers();
require 'nylas'

nylas = Nylas::Client.new(api_key: '{NYLAS_API_KEY}')

response = nylas.notetakers.list(
  identifier: '{GRANT_ID}'
)

puts "Notetakers:"
response.data.each do |notetaker|
  puts "- #{notetaker[:id]}: #{notetaker[:name]} (Status: #{notetaker[:status]})"
end
from nylas import Client

nylas = Client(
    api_key="{NYLAS_API_KEY}",
)

response = nylas.notetakers.list(
    identifier="{GRANT_ID}"
)

print(f"Notetakers: {response.data}")
import com.nylas.NylasClient;
import com.nylas.models.*;

public class ListNotetakers {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("{NYLAS_API_KEY}").build();

    ListResponse<Notetaker> response = nylas.notetakers().list("{GRANT_ID}");

    System.out.println("Notetakers:");
    for (Notetaker notetaker : response.getData()) {
      System.out.println("- " + notetaker.getId() + ": " + notetaker.getName() +
                         " (Status: " + notetaker.getStatus() + ")");
    }
  }
}
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/{GRANT_ID}/notetakers" \
  --header "Authorization: Bearer {NYLAS_API_KEY}"
import com.nylas.NylasClient

fun main() {
    val nylas = NylasClient(apiKey = "{NYLAS_API_KEY}")

    val response = nylas.notetakers().list("{GRANT_ID}")

    println("Notetakers:")
    response.data.forEach { notetaker ->
        println("- ${notetaker.id}: ${notetaker.name} (Status: ${notetaker.status})")
    }
}

The Nylas API will use the grantId in the URL (<GRANT_ID>) to return all notetakers for that user. If everything is set up correctly, this call returns data (likely empty) of any existing Notetaker. It confirms that your API key and grant are valid.

If you haven’t obtained a grant ID yet, you’ll need to authorize a user through Nylas. This typically involves directing the user to Nylas’ hosted authentication page and obtaining an authorization code which you exchange for a grantId. For the purposes of this post, consider using the Nylas dashboard to create a test grant:

Once you have the grantId for the user’s account, you can proceed to invite a Notetaker on their behalf. At this point, with your API key and a grant ID, we can now move on to inviting the Notetaker to meetings.

(Optional) Create an event using Nylas Events API

This section is optional, and requires adding the relevant event scopes before creating the user grant. Let’s start off by using the Nylas Events API to create an event which we can invite a Nylas Notetaker to:

Node.js

Ruby

Python

Java

Curl

Kotlin

Response

// Route to create an event on a calendar
app.get("/nylas/create-event", async (req, res) => {
  try {
    const grantId = fetchUserGrantIdFromYourDb(); // This is an example, you'll have to write this
    const calendarId = "primary";

    // Schedule the event to start in 5 minutes and end in 35 minutes
    const now = new Date();
    const startTime = new Date(now.getTime());
    startTime.setMinutes(now.getMinutes() + 5);
    const endTime = new Date(now.getTime());
    endTime.setMinutes(now.getMinutes() + 35);

    const newEvent = await nylas.events.create({
      identifier: grantId,
      queryParams: {
        calendarId,
      },
      requestBody: {
        title: "Your event title here",
        when: {
          startTime: Math.floor(startTime.getTime() / 1000), // Time in Unix timestamp format (in seconds)
          endTime: Math.floor(endTime.getTime() / 1000),
        },
        conferencing": {    
          provider: "Google Meet",
          autocreate: {}
        }
      },
    });

    res.json(newEvent);
  } catch (error) {
    console.error("Error creating event:", error);
  }
});
# To handle time manipulation
class Numeric
  def minutes
    self / 1440.0
  end
  alias minute minutes

  def seconds
    self / 86_400.0
  end
  alias second seconds
end

get '/nylas/create-event' do
  now = DateTime.now
  now += 5.minutes

  start_time = Time.local(now.year, now.month, now.day,
      now.hour, now.minute, now.second).strftime('%s')

  now += 35.minutes

  end_time = Time.local(now.year, now.month, now.day,
      now.hour, now.minute, now.second).strftime('%s')

  query_params = { calendar_id: "primary" }

  request_body = {
    when: {
      start_time: start_time.to_i,
      end_time: end_time.to_i
    },
    title: 'Your event title here',
    conferencing: {
      provider: "Google Meet",
      autocreate: {}
    }
  }

  event, = nylas.events.create(identifier: session[:grant_id],
      query_params: query_params, request_body: request_body)

  event.to_json
rescue StandardError => e
  e.to_s
end
@app.route("/nylas/create-event", methods=["GET"])
def create_event():
  now = datetime.now()
  now_plus_5 = now + timedelta(minutes = 5)

  start_time = int(datetime(now.year, now.month, now.day, now_plus_5.hour,
      now_plus_5.minute, now_plus_5.second).strftime('%s'))

  now_plus_35 = now_plus_5 + timedelta(minutes = 35)

  end_time = int(datetime(now.year, now.month, now.day, now_plus_35.hour,
      now_plus_35.minute, now_plus_35.second).strftime('%s'))

  query_params = {"CALENDAR_ID": "primary"}

  request_body = {
    "when": {
      "start_time": start_time,
      "end_time": end_time,
    },
    "title": "Your event title here",
    "conferencing": {    
      "provider": "Google Meet",
      "autocreate": {}
    }
  }

  try:
    event = nylas.events.create(session["NYLAS_GRANT_ID"],
        query_params = query_params, request_body = request_body)

    return jsonify(event)
  except Exception as e:
    return f'{e}'
get("/nylas/create-event", (request, response) -> {
  try {
    LocalDate today = LocalDate.now();
    Instant instant = today.atStartOfDay(ZoneId.systemDefault()).toInstant();
    Instant now_plus_5 = instant.plus(5, ChronoUnit.MINUTES);
    long startTime = now_plus_5.getEpochSecond();
    Instant now_plus_10 = now_plus_5.plus(35, ChronoUnit.MINUTES);
    long endTime = now_plus_10.getEpochSecond();

    CreateEventRequest.When.Timespan timespan = new CreateEventRequest.When.Timespan.
        Builder(Math.toIntExact(startTime),
        Math.toIntExact(endTime)).
        build();

    Map<String, Object> conferencing = new HashMap<>();
    conferencing.put("provider", "Google Meet");
    conferencing.put("autocreate", new HashMap<>()); // Empty object

    CreateEventRequest createEventRequest = new CreateEventRequest.Builder(timespan)
        .title("Your event title here")
        .build();

    eventBuilder.conferencing(conferencing);

    CreateEventQueryParams createEventQueryParams = new CreateEventQueryParams.
        Builder("primary").
        build();

    Event event = nylas.events().create(request.session().attribute("NYLAS_GRANT_ID"),
        createEventRequest, createEventQueryParams).getData();

    return "%s".formatted(event);
  } catch (Exception e) {
    return "%s".formatted(e);
  }
});
curl --request POST \
  --url https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=<CALENDAR_ID> \
  --header 'Accept: application/json, application/gzip' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Nylas Integration",
    "busy": true,
    "participants": [
      {
        "name": "Nyla",
        "email": "[email protected]"
      },
      {
        "name": "Ram",
        "email": "[email protected]"
      }
    ],
    "description": "Come ready to talk Nylas!",
    "when": {
        "start_time": 1674604800,
        "end_time": 1722382420,
    },
    # add config to autocreate a Google Meet link
    "conferencing": {    
        "provider": "Google Meet",
        "autocreate": {}
    }
}'   
http.get("/nylas/create-event") {
  try {
    val today = LocalDate.now()
    val instant = today.atStartOfDay(ZoneId.systemDefault()).toInstant()
    val nowPlus5 = instant.plus(5, ChronoUnit.MINUTES)
    val startTime = nowPlus5.epochSecond
    val nowPlus10 = nowPlus5.plus(35, ChronoUnit.MINUTES)
    val endTime = nowPlus10.epochSecond

    val eventWhenObj: CreateEventRequest.When = CreateEventRequest.When.Timespan(startTime.toInt(), endTime.toInt())

    val conferencing = mapOf(
      "provider" to "Google Meet",
       "autocreate" to emptyMap<String, Any>()
    )

    val eventRequest: CreateEventRequest = CreateEventRequest.Builder(eventWhenObj).
        title("Your event title here").conferencing(conferencing).build()

    val eventQueryParams: CreateEventQueryParams = CreateEventQueryParams("primary")

    val event: Response<Event> = nylas.events().create(request.session().attribute("NYLAS_GRANT_ID"),
        eventRequest, eventQueryParams)

    event
  } catch (e : Exception) {
    e.toString()
  }
}
{
  "request_id": "1",
  "data": {
    ...event details...
    "conferencing": {
      "details": {
        "meeting_code": "<MEETING_CODE>",
        "url": "<MEETING_URL>"
      },
      "provider": "Google Meet"
    },
    ...more event details...
  }
}

Creating an event will return the following relevant details, note the <MEETING_URL> that we will use in upcoming sections.

Adding Notetaker to a meeting

Now for the fun part: inviting the Notetaker to a meeting. Nylas Notetaker can join meetings in two ways:

  • On-demand (immediate join): You invite the Notetaker to an ongoing meeting without specifying a start time. The Notetaker will attempt to join right away.
  • Scheduled join: You schedule the Notetaker to join a future meeting at a specified time by providing the meeting start time. Providing a time to join is optional.

In both cases, you will use the Nylas API to invite the Notetaker by creating a Notetaker instance for a given meeting. This is done via a POST request to the Notetaker endpoint.

Invite the Notetaker via API: To invite the bot, make a POST request with the meeting information. You’ll need to provide the meeting’s meeting_link. Here’s an example:

Node.js

Ruby

Python

Java

Curl

Kotlin

Response

const Nylas = require("nylas");
const nylas = new Nylas({
  apiKey: "{NYLAS_API_KEY}",
});

async function createNotetaker() {
  const response = await nylas.notetakers.create({
    identifier: "{EMAIL}",
    requestBody: {
      meetingLink: "{MEETING_LINK}",
      name: "My Notetaker",
    },
  });

  console.log("Notetaker created:", response);
}

createNotetaker();
require 'nylas'

nylas = Nylas::Client.new(api_key: '{NYLAS_API_KEY}')

response = nylas.notetakers.create(
  identifier: '{EMAIL}',
  request_body: {
    meeting_link: '{MEETING_LINK}',
    name: 'My Notetaker'
  }
)

puts "Notetaker created: #{response.inspect}"
from nylas import Client

nylas = Client(
    api_key="{NYLAS_API_KEY}",
)

response = nylas.notetakers.create(
    identifier="{EMAIL}",
    request_body={
        "meeting_link": "{MEETING_LINK}",
        "name": "My Notetaker"
    }
)

print(f"Notetaker created: {response}")
import com.nylas.NylasClient;
import com.nylas.models.*;

public class CreateNotetaker {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("{NYLAS_API_KEY}").build();

    CreateNotetakerRequest request = new CreateNotetakerRequest.Builder()
        .meetingLink("{MEETING_LINK}")
        .name("My Notetaker")
        .build();

    Response<Notetaker> response = nylas.notetakers().create("{EMAIL}", request);

    System.out.println("Notetaker created: " + response.getData());
  }
}
curl --request POST "https://api.nylas.com/v3/grants/<GRANT_ID>/notetakers" \
     --header "Authorization: Bearer <NYLAS_API_KEY>" \
     --header "Content-Type: application/json" \
     --data '{
           "meeting_link": "<MEETING_URL>", 
           "join_time": "1732657774",
           "notetaker_name": "Nylas Notetaker",
           "meeting_settings": {
             "video_recording": true,
             "audo_recording": true,
             "transcription": true,
           }
         }'
import com.nylas.NylasClient
import com.nylas.models.CreateNotetakerRequest

fun main() {
    val nylas = NylasClient(apiKey = "{NYLAS_API_KEY}")

    val request = CreateNotetakerRequest(
        meetingLink = "{MEETING_LINK}",
        name = "My Notetaker"
    )

    val response = nylas.notetakers().create("{EMAIL}", request)

    println("Notetaker created: ${response.data}")
}
{
 "request_id":"<REQUEST_ID>",
  "data":{"id":"<NOTETAKER_ID>",
  "grant_id":"<GRANT_ID>",
  "name":"Nylas Notetaker",
  "state":"connecting",
  "meeting_link":"<MEETING_LINK>",
  "meeting_provider":"Google Meet",
  "meeting_settings":{
    "video_recording":true,
    "audio_recording":true,
    "transcription":true,
  }
  "join_time":1741210298
}

In the above snippet:

  • meeting_link is a valid link to your online meeting. This could be a Google Meet, Microsoft Teams link, Zoom meeting link, etc.
  • join_time is the scheduled start time of the meeting in seconds (unix time). If you leave out join_time, the bot will try to join as soon as the request is made.

A successful request will return a JSON response containing a unique notetakerId for the bot instance created.

Keep note of the notetakerId—it’s how you will reference this specific Notetaker session in subsequent API calls (like fetching transcripts or updating the notetaker).

What happens next? When the scheduled time arrives (or immediately, if no time was given), Nylas will have the Notetaker bot join the meeting. Nylas is working in the background to get your bot into the meeting. If the meeting hasn’t started or the join link isn’t active yet, the bot will keep trying until it can join or until it times out. If the meeting is restricted (organization-only, waiting room enabled, etc.), someone might need to admit the “guest” user (the Notetaker) into the call:

Syncing Notetaker with calendar events

One interesting scenario that we may come across is when a calendar event is updated that a Notetaker has been invited to. By default, the Notetaker will not know when a calendar event is updated, so it will not be able to join the updated meeting time:

Node.js

Ruby

Python

Java

Curl

Kotlin

const Nylas = require("nylas");
const nylas = new Nylas({
  apiKey: "{NYLAS_API_KEY}",
});

async function createNotetaker() {
  const response = await nylas.notetakers.create({
    identifier: "{GRANT_ID}",
    requestBody: {
      meetingLink: "{MEETING_LINK}",
      name: "My Notetaker",
    },
  });

  console.log("Notetaker created:", response);
}

createNotetaker();
require 'nylas'

nylas = Nylas::Client.new(api_key: '{NYLAS_API_KEY}')

response = nylas.notetakers.create(
  identifier: '{GRANT_ID}',
  request_body: {
    meeting_link: '{MEETING_LINK}',
    name: 'My Notetaker'
  }
)

puts "Notetaker created: #{response.inspect}"
from nylas import Client

nylas = Client(
    api_key="{NYLAS_API_KEY}",
)

response = nylas.notetakers.create(
    identifier="{GRANT_ID}",
    request_body={
        "meeting_link": "{MEETING_LINK}",
        "name": "My Notetaker"
    }
)

print(f"Notetaker created: {response}")
import com.nylas.NylasClient;
import com.nylas.models.*;

public class CreateNotetaker {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("{NYLAS_API_KEY}").build();

    CreateNotetakerRequest request = new CreateNotetakerRequest.Builder()
        .meetingLink("{MEETING_LINK}")
        .name("My Notetaker")
        .build();

    Response<Notetaker> response = nylas.notetakers().create("{GRANT_ID}", request);

    System.out.println("Notetaker created: " + response.getData());
  }
}
curl --request POST "https://api.nylas.com/v3/grants/<GRANT_ID>/notetakers" \
     --header "Authorization: Bearer <NYLAS_API_KEY>" \
     --header "Content-Type: application/json" \
     --data '{
           "meeting_link": "<MEETING_URL>", 
           "join_time": "1732657774",
           "notetaker_name": "Nylas Notetaker",
           "meeting_settings": {
             "video_recording": true,
             "audo_recording": true,
             "transcription": true,
           }
         }'
import com.nylas.NylasClient
import com.nylas.models.CreateNotetakerRequest

fun main() {
    val nylas = NylasClient(apiKey = "{NYLAS_API_KEY}")

    val request = CreateNotetakerRequest(
        meetingLink = "{MEETING_LINK}",
        name = "My Notetaker"
    )

    val response = nylas.notetakers().create("{GRANT_ID}", request)

    println("Notetaker created: ${response.data}")
}

Check out our developer documentation to learn more about calendar and event sync.

Now we’ve created a calendar event that includes the Notetaker, so anytime the event time is updated, the Notetaker will sync and join at the updated time.

Handling Notetaker notifications and webhooks

To build a scalable integration, let’s go over how to handle real-time notifications for Notetaker activities. Nylas uses webhooks to inform your application of events such as the bot being created, joining a meeting, or the transcription being ready. Let’s go over how to set up webhooks.

Subscribe to Notetaker webhook events: In the Nylas dashboard or via API, you need to register a webhook URL (an endpoint on your server) and specify which events you want to receive. The Notetaker supports several event triggers.

You can choose which of these events to register as a webhook. At minimum, subscribing to notetaker.media is highly useful to know when to grab the transcript.

To create a webhook subscription via the API, you would send a POST request to the /v3/webhooks endpoint:

Node.js

Ruby

Python

Java

Curl

Kotlin

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}`);
});
# 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
# 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()
//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";
    });
  }
}
curl --request POST "https://api.us.nylas.com/v3/webhooks" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer $NYLAS_API_KEY" \
     -d '{
           "trigger_types": ["notetaker.media", "notetaker.meeting_state"],
           "description": "Notetaker updates webhook",
           "webhook_url": "https://<YOUR_APP_URL>/webhook",
           "notification_email_addresses": ["[email protected]"]
         }'
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")
    )
  }
}

In the above JSON, we subscribe to two trigger events (notetaker.media and notetaker.meeting_state).

Security tip: Nylas webhooks can be secured by verifying signatures. Check out a recent blog post on creating and securing Nylas webhooks. This will ensure nobody can spoof events to your endpoint.

With webhooks in place, your integration becomes event-driven. You don’t have to continuously check if the meeting started or if the transcript is ready; Nylas will push that info to you in real time.

Retrieving meeting transcriptions

Once your meeting has concluded, the bot will leave and begin processing the recording to generate a transcription. As a developer, you have two ways to retrieve the meeting’s transcription and recording:

1. Via Webhook Notifications (Real-time): If you’ve subscribed to the notetaker.media webhook event, Nylas will send a notification to your webhook URL when the transcription and recording are ready. The webhook payload will include temporary URLs for the video recording and the transcription file.

2. Via API Request (On-demand): You can also fetch the recording and transcript by making a GET request to the Notetaker media endpoint. This request will return JSON containing URLs for the recording and transcript, similar to the webhook data.

Let’s look at option #2 with a quick API call to get the transcript after a meeting:

Node.js

Ruby

Python

Java

Curl

Kotlin

Response

const Nylas = require("nylas");
const nylas = new Nylas({
  apiKey: "{NYLAS_API_KEY}",
});

async function listNotetakers() {
  const response = await nylas.notetakers.list({
    identifier: "{GRANT_ID}",
  });

  console.log("Notetakers:", response.data);
}

listNotetakers();
require 'nylas'

nylas = Nylas::Client.new(api_key: '{NYLAS_API_KEY}')

response = nylas.notetakers.list(
  identifier: '{GRANT_ID}'
)

puts "Notetakers:"
response.data.each do |notetaker|
  puts "- #{notetaker[:id]}: #{notetaker[:name]} (Status: #{notetaker[:status]})"
end
from nylas import Client

nylas = Client(
    api_key="{NYLAS_API_KEY}",
)

response = nylas.notetakers.list(
    identifier="{GRANT_ID}"
)

print(f"Notetakers: {response.data}")
import com.nylas.NylasClient;
import com.nylas.models.*;

public class ListNotetakers {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("{NYLAS_API_KEY}").build();

    ListResponse<Notetaker> response = nylas.notetakers().list("{GRANT_ID}");

    System.out.println("Notetakers:");
    for (Notetaker notetaker : response.getData()) {
      System.out.println("- " + notetaker.getId() + ": " + notetaker.getName() +
                         " (Status: " + notetaker.getStatus() + ")");
    }
  }
}
curl --request GET "https://api.nylas.com/v3/grants/<GRANT_ID>/notetakers/<NOTETAKER_ID>/media" \
     --header "Authorization: Bearer <NYLAS_API_KEY>
import com.nylas.NylasClient

fun main() {
    val nylas = NylasClient(apiKey = "{NYLAS_API_KEY}")

    val response = nylas.notetakers().list("{GRANT_ID}")

    println("Notetakers:")
    response.data.forEach { notetaker ->
        println("- ${notetaker.id}: ${notetaker.name} (Status: ${notetaker.status})")
    }
}
{
  "request_id":"<REQUEST_ID>",
  "data":{
    "recording":{"url":"<URL>","size":0, ...more details...},
    "transcript":{"url":"<URL>","size":0, ...more details...}
  }
}

The url fields are the direct download links for the video recording and the text transcript of the meeting.

By using either the webhook or manual fetch approach, you can retrieve the full meeting transcript and then integrate it into your application.

Build Time!

In this blog post, we went over the steps to integrate the Nylas Notetaker into your meetings to automatically record and transcribe audio from meetings.

You can sign up for Nylas for free and start building! Continue building with Nylas by exploring different quickstart guides or by visiting our developer documentation.

Related resources

How to build an AI-powered meeting notes generator

Learn how to use AI to generate meeting notes with Nylas Notetaker APIs.

How to Integrate Gmail with Cursor

Learn how to build a gmail integration fast with Cursor Agent and Nylas APIs in this step-by-step guide—perfect for developers and small teams.

How to Build a CRM with Replit AI Agent: A Step-by-Step Guide

Learn how to build a CRM fast with Replit AI Agent and Nylas APIs in this step-by-step guide—perfect for developers and small teams.