Using Webhooks to Integrate Google Calendar and React


In this tutorial, you’ll learn how to build a React chat app that integrates the Google Calendar API and Stream’s React Chat SDK. By the end, you’ll configure a custom webhook that retrieves your calendar events and lists them in a chat channel.

Use Webhooks to Integrate Google Calendar and React

Many chat apps today implement /slash commands for their end-users. When done right, these commands can be both practical and engaging, serving a variety of use cases.

For this tutorial, you’ll create a custom /gcal command that will populate your app's chat channel with your upcoming Google Calendar events and call it with a webhook you'll configure in Stream's dashboard.

As an added bonus, you’ll implement Google’s OAuth 2.0 flow to access Google APIs in your app.

Developer Setup and Prerequisites

This project uses the following stack:

  • React.js
  • Express
  • Node.js
  • SQLite

Note: If you don’t know any SQL, that’s ok! By the end of this tutorial, you will be able to write simple SQL queries to execute basic CRUD operations in your server.

In addition to these technologies, you will also need:

  • Gmail
  • Google Calendar
  • Google Cloud Platform
  • Ngrok (or another tunneling service)

Before you start: Make sure you've installed Homebrew and the most recent version of Node.js.

To install Homebrew, run the following command:

$ /bin/bash -c "$(curl -fsSL"

You’ll also need npm (version 5.2+) so you can create a project using Create React App (CRA).

With CRA installed, create a new React project:

$ npx create-react-app google-int-react

Then, create a folder for your server:

$ mkdir google-int-react-be
$ npm init -y 

Stream Setup

Next, create a free Stream account. You’ll get a free 30-day trial so you can see what all Stream Chat has to offer.

After creating an account, go to your Stream Dashboard and create a project:

  1. Select Create App.
  2. Enter an App Name (like React Google Cal Integration).
  3. Select your Feeds Server and Data Storage locations.
  4. Set your Environment to Development.

Go into your project and store your API Key, Secret, and App ID somewhere safe so you can reference them later.

If you want to learn more about integrating Stream chat with your app, check out the React chat SDK tutorial.

Google Cloud Platform Setup

To use Google’s services in your app, you need to create a project on Google Cloud Platform. This requires you to create an account if you haven’t already.

Once you have an account, create a project:

  1. In your Cloud Console dashboard, go to the Manage Resources page.
  2. Select Create Project.
Create project in Google Cloud Console
  1. In the New Project window, enter a Project name (like React Google Integration).
  2. In the Organization dropdown, select No organization.
  3. Enter the parent organization or folder in the Location box.
  4. Select Create.

Create a Client ID

This project uses Google Sign-In, which implements the OAuth 2.0 flow to integrate and manage the use of Google APIs in your app.

If you’re unfamiliar with OAuth 2.0, read up on Access Tokens, Refresh Tokens, and Authorization Codes.

To use Google Sign-In, you need a Client ID:

  1. In your Google Cloud dashboard, click the Navigation menu.
  2. Go to APIs & Services and select Credentials.
Creating project credentials
  1. In the Credentials window, select CREATE CREDENTIALS, then OAuth client ID.
Creating client ID step 1
  1. In the Application type dropdown, select Web application.
  2. Enter a Name for your Web client ID.
  3. Under Authorized JavaScript origins, enter the two localhost ports you plan on using for your client and server.
Entering Javascript URIs

This project uses localhost:3000 for the client and localhost:3001 for the server.

  1. Select CREATE.
OAuth client created

You’re all set! Now, you should see your client ID listed on the Credentials page.

Ngrok Setup

You can use an alternative tunneling service, but the free ngrok plan will suffice for this project.

To get started:

  1. Go to the ngrok Signup page and enter your Name, E-mail, and Password.
Ngrok Signup
  1. Click Sign Up.
  2. In your ngrok dashboard under Getting Started > Setup & Installation, download ngrok for the appropriate OS.
Ngrok installation steps
  1. Then, follow the ngrok installation instructions.

Test your ngrok connection with the following command:

$ ngrok http 80

If installed correctly, you’ll see a new terminal window labeled ngrok http 80.

Ngrok connection test

Build Your React Client

With all of your accounts set up, you can organize your project. When bootstrapping a project with CRA, you’ll end up with some unnecessary files, so start by clearing those out.

Under src, delete the following files:

You should also delete any code in App.js and App.css.

In index.js, replace the code with the following:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

Cd into src and create your components folder:

$ mkdir components

Then, create your Auth.jsx and ChannelContainer.jsx components:

$ touch Auth.jsx ChannelContainer.jsx

Lastly, create a .env file in your root project folder:

$ touch .env 

Your project folder structure should look similar to the image below:

React project folder structure

With your client set up, now you need to install the following dependencies:

$ npm i dotenv react-google-login stream-chat stream-chat-react universal-cookie

Set Up Your Client’s Environment Variables

After installing your dependencies, you should create your environment variables.

You’ll need your:

  • Stream API Key
  • Google Client ID

Place these values into your .env file:


Initialize Stream Chat

Now, you need to initialize your client with Stream, which requires your API Key.

In App.jsx, add the following code:

import React from "react";
import { StreamChat } from "stream-chat";
import { Chat } from "stream-chat-react";
const client = StreamChat.getInstance(process.env.REACT_APP_STREAM_API_KEY);

const LogoutButton = ({ logout }) => (<button className="logout-button" onClick={logout}>Logout</button>);

const App = () => {

const logout = () => {}

return (
   <div className="app__wrapper">
     <Chat client={client}>
 Chat dashboard

export default App;

In this snippet, you:

  • Pass in your API Key to StreamChat.getInstance() to create your client instance.
  • Wrap your app in the Chat context wrapper.
  • Return your app’s “Chat dashboard”.

All of your components will sit inside Stream’s Chat context wrapper, providing your app with all the out-of-the-box chat functionality it needs to create a messaging channel.

Build a Simplified Auth Flow

Now, you’ll build a simplified auth flow that uses cookies to set and remove a mock temp_token to log the user in and out of the chat dashboard (later, you’ll generate a unique Stream token in the backend).

In Auth.jsx, add the following code:

import React from 'react';
import Cookies from "universal-cookie";

const cookies = new Cookies();
const tempToken = "temp_token";

const signIn = () => {
   cookies.set('temp_token', tempToken);

const Auth = () => {
   return (
           <button onClick={signIn}>Sign in</button>

export default Auth;

In Auth.jsx, you:

  • Instantiate your cookies and create a tempToken variable.
  • Declare your signIn() function, which listens for an on-click event to set the temp_token and trigger a page reload.

Now, add the following code in App.jsx:

import React from "react";
import { StreamChat } from "stream-chat";
import { Chat } from "stream-chat-react";
import Auth from "./Auth"
import Cookies from "universal-cookie";

const cookies = new Cookies();
const client = StreamChat.getInstance(process.env.REACT_APP_STREAM_API_KEY);

const authToken = cookies.get("temp_token");

const LogoutButton = ({ logout }) => (<button className="logout-button" onClick={logout}>Logout</button>);

const App = () => {

const logout = () => {
 console.log("logout worked")

 if (!authToken) return <Auth/>;

 return (
   <div className="App">
     <Chat client={client}>
       Chat dashboard
       <LogoutButton logout={logout}/>

export default App;

In the snippet above, you declare a logout() function that removes your temp_token when the user clicks the logout button. It also triggers a page reload.

Now, when a user logs in, the authToken will trigger a page reload and redirect them to the chat dashboard. Clicking logout removes the token and triggers a page reload, directing the user to the sign-in page.

Lastly, add the following code to your App.css file:

body {
 text-align: center;
 padding-top: 15%;

button {
 margin-left: 10px;

You should be able to sign in and out of your app:

Building Your Backend

With your basic front-end structure out of the way, you can build your backend:

Open your backend project. In your project folder, create the following folders and files with the commands below:

$ mkdir controllers database models routes 
$ touch .env index.js 

Your project structure should look similar to the one below:

Server project folder structure

To get your project running, install the following dependencies:

$ npm i cors crypto dotenv express getstream google-auth-library googleapis nodemon sqlite3 stream-chat

With all your dependencies installed, configure your script in package.json so you can easily run nodemon to automatically restart your server after you save changes:

"scripts": {
   "start": "node index.js",
   "dev": "nodemon index.js"

Now, go to index.js and add the following code:

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
// const authRoutes = require("./routes/auth");

const app = express();

   verify: (req, res, buf) => {
       req.rawBody = buf;

app.use((request, response, next) => {

const PORT = "3001";


app.get('/', (req, res) => {
   res.send("hello world");

// app.use('/auth', authRoutes);

app.listen(PORT, () => {
   console.log(`server running on http://localhost:${PORT}`);

Now, run npm run dev in your terminal. Your server should greet you with a “hello world” on your localhost port in your browser 😎

Define Your Environment Variables

In your .env file, you’ll need your Stream and Google secrets so you can use them throughout your server.

You can find all the required values in your Stream dashboard and under your OAuth 2.0 Client IDs in the Credentials section of your Google Cloud Platform dashboard.

Once you’ve entered those values, your .env file should look similar to the file below:


Define Your Routes and Controllers

In your ./routes folder, create a new file called auth.js:

$ touch auth.js 

In routes/auth.js, enter the following code:

const express = require('express');

const { login, googleauth, handlewebhook } = require("../controllers/auth");

const router = express.Router();'/login', login);'/googleauth', googleauth);'/handlewebhook', handlewebhook)

module.exports = router;

In the snippet above, you direct your server’s routes to the specified endpoints. But if you fire up your server now, it should throw an error. That’s because you haven’t defined these endpoints in ../controllers/auth yet.

Back in your controllers folder, create another auth.js file:

$ touch auth.js

Now, go to controllers/auth.js and define the handlers you’ll need for your backend with the following code:


const login = async (req, res) => {}
const googleauth = async (req, res) => {}

Note: Later, you’ll define setupCommands() and handlewebhook() functions to handle your webhook. For now, you just need login() and googleauth().

Before continuing, you’ll need some way to persist your user’s Stream data, which calls for a database.

Implement Your SQLite Database

SQLite is a lightweight SQL database engine that’s perfect for the scope of this project.

To get started, you’ll need to install SQLite. If you use a Mac, it should be installed already. You can check by entering sqlite3 into your terminal. If it’s installed, you’ll see the following response in your terminal:

SQLite installation check

If it’s not installed on your machine, go to the SQLite download page and follow the instructions.

Once installed, enter your database folder and create the following files:

  • db.js: This is where you’ll configure your database connection
  • db.sql: This is where you’ll create your users table
  • proj.db: This is a simple text file that will contain your project’s in-memory database

In database, run the following command:

$ touch db.js db.sql proj.db 

In db.js, enter the following code:

const sqlite3 = require("sqlite3").verbose();
const db = new sqlite3.Database('database/proj.db', sqlite3.OPEN_READWRITE, (err) => {
   if (err) return console.error(err.message);
   console.log("connection to db successful");

module.exports = db;

If you fire up your server, you should see “connection to db successful” in your console.

Now, you're ready to create your users table.

In db.sql, enter the following code:

   name TEXT DEFAULT "",
   email TEXT DEFAULT "",
   user_id TEXT DEFAULT "", 
   refresh_token TEXT DEFAULT ""

This is a basic SQL command that tells your database to create a table if it doesn't exist. In this table, you specify what columns you need and what data types your DB should expect when you insert data.

This is a straitforward DB schema, where the data will be of the type TEXT, which means you can insert your data as a String.

Enter the following command to create your table:

$ sqlite3 database/proj.db
$ .read database/db.sql

To see if you created your users table successfully, enter .tables in your terminal. You should see your table listed:

Check users table in SQLite database

With your database up and running, you can define some basic CRUD operations for your app.

Go to your models folder and create a file called data-models.js:

$ touch data-models.js

In data-models.js, enter the following code:

const db = require("../database/db");

   function findUser(email) {
      return new Promise((resolve, reject) => {
       const getUser = `SELECT * FROM users WHERE email="${email}"`;
           db.get(getUser, [], (err, user) => {
               if(err) console.log(err.message);

   function insertUsers(name, email, user_id) {
       return new Promise((resolve, reject) => {
           const insertUser = `INSERT INTO users (name, email, user_id) VALUES(?,?,?)`;
   , [name, email, user_id], function (err) {
                   if (err) return console.error(err.message);
                   const rowID = this.lastID;

   function updateRefreshToken(refresh_token, email) {
       const insertToken = `UPDATE users SET refresh_token = ? WHERE email = ?`;, [refresh_token, email], function (err) {
           if(err) return console.error("SQL error:", err.message);

   function getRefreshToken(email) {
       return new Promise((resolve, reject) => {
           const getToken = `SELECT refresh_token FROM users WHERE email="${email}"`;
           db.get(getToken, [], (err, token) => {
               if(err) console.log(err.message);
   function getRefreshTokenWithId(user_id) {
       return new Promise((resolve, reject) => {
           const getToken = `SELECT refresh_token FROM users WHERE user_id="${user_id}"`;
           db.get(getToken, [], (err, token) => {
               if(err) console.log(err.message);

module.exports = { findUser, insertUsers, updateRefreshToken, getRefreshToken, getRefreshTokenWithId }

In this code block, you define and export several functions that you’ll use to interact with your database:

  • findUser(email): Checks if a user exists in your database with their email.
  • insertUsers(name, email, user_id): Creates a new user by inserting their name, email, and user_id into your database’s users table.
  • updateRefreshToken(refresh_token, email): Gives the user a new refresh_token.
  • getRefreshToken(email): Retrieves the user’s refresh token with their email.
  • getRefreshTokenWithId(user_id): Retrieves the user’s refresh token with their user_id.

That’s it for your database! Now, back to your endpoints.

Backend Auth Flow With Stream and Google

Now, you can set up your Stream and Google auth flow.

Back in controllers/auth.js, replace your code with the following:

const {StreamChat} = require('stream-chat');
const crypto = require('crypto');
const dataModel = require("../models/data-model");

// Google Cal integration
const {google} = require('googleapis');
const { OAuth2Client } = require('google-auth-library');

const scopes = ['', ''];

// Set Google and Stream environment variables
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
const googleRedirectUrl = process.env.GOOGLE_REDIRECT_URL;

const api_key = process.env.STREAM_API_KEY;
const api_secret = process.env.STREAM_SECRET;
const app_id = process.env.STREAM_ID;

// Instantiate Google OAuth 2.0 client
const oAuth2Client = new google.auth.OAuth2(

// set auth as a global default
   auth: oAuth2Client

const login = async (req, res) => {
   try {
       // Google Auth
       const googleToken = req.body.token;

       const ticket = oAuth2Client.verifyIdToken({
           idToken: googleToken,
           audience: process.env.GOOGLE_CLIENT_ID

       const {name, email} = (await ticket).getPayload()
       const url = oAuth2Client.generateAuthUrl({
           access_type: 'offline',
           scope: scopes

       // Stream Auth
       const serverClient = StreamChat.getInstance(api_key, api_secret, app_id);
       // await setupCommands(serverClient);

       // SQL Queries -- Find user if exists; otherwise, insert new user
       const userFound = await dataModel.findUser(email);
       if(!userFound) {
           const user_id = crypto.randomBytes(16).toString('hex');

           dataModel.insertUsers(name, email, user_id).then((newUser) => {

           const token = serverClient.createToken(user_id);
           return res.status(200).json({token, user_id, name, email, url})

       // Pass in user_id and generate Stream token
       const user_id = userFound.user_id;
       const token = serverClient.createToken(user_id);

       res.status(200).json({token, user_id, name, email, url})
   } catch (error) {
       res.status(500).json({message: error})

Note what’s happening in this code block:

  • You import googleapis and the OAuth2Client library.
  • You declare your scopes, which will prompt the user to give their consent for you to access their Google Calendar data.
  • You declare your Stream and Google .env variables.
  • You instantiate your Google OAuth 2.0 (oAuth2Client) client using the google.auth.OAuth2 constructor.

Note: The MARKDOWN_HASH957498a5940da078121d2fc7f984da47MARKDOWNHASH instance is important, as it keeps track of your refresh and access_tokens for you. You'll use it in multiple places throughout your server.

Here’s what’s happening in login:

  • You handle your Google token and profile object (you’ll set this up in your client soon), which generates an authorization url. This url is necessary to generate the consent form for your users, and specifies your permission scopes.
  • You initialize your server-side Stream client with your .env variables.
  • Your dataModel.findUser() function checks if there’s a user with the same email in the database. If no user exists, you generate a unique Stream user_id and call insertUsers to create the user in the database and generate their Stream token.
  • If the user does exist, you pass in the user_id to create a new Stream token.
  • Finally, you send your token, user_id, name, email, and url back to your front-end client.

With this data, you can authenticate a user on your front-end with a Stream token, replacing the mock token you used earlier. You can also prompt the user to give you access to their Google Calendar data.

Integrating Google Sign-In

Back in your client, you’ll add React Google Login to handle your Google sign-in.

In Auth.jsx, replace the file with the following code:

import React from 'react';
import Cookies from "universal-cookie";
import GoogleLogin from "react-google-login";

const cookies = new Cookies();

const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID;

const Auth = () => {

 const responseGoogle = (googleRes) => {
   const data = googleRes.profileObj;
   console.log(`Google auth response ${googleRes}`)
   const URL = `http://localhost:3001/auth/login`;

   fetch(URL, {
     method: 'POST',
     headers: {
       'Content-type': 'application/json'
     body: JSON.stringify({ data: data, token: googleRes.tokenId })
   }).then((res) => res.json()).then((userData) => {

     const { name, email, token, user_id, url } = userData;
     cookies.set('name', name);
     cookies.set('email', email);
     cookies.set('token', token);
     cookies.set('user_id', user_id);

 return (
   <div className="auth__form-container">
     <span><p className="signup-text">Sign Up With Google:</p>
           buttonText="Sign In"


export default Auth;

In this snippet:

  • You place the GoogleLogin component in your return statement.
  • You pass in your clientId as a prop and the responseGoogle function to handle Google’s response when you sign in.
    • In responseGoogle, you’re expecting a profile object, which includes your name, email, and other data related to your Google account.
  • Next, you specify the URL endpoint you want to hit in your server and create your fetch request, which must include the data and tokenId from the profile object.
  • Upon a successful request, you’ll receive your name, email, Stream token, Stream user_id, and Google’s unique authorization url. You’ll store the name, email, token, and user_id in cookies with cookies.set().
  • Lastly, you use window.location.assign(url) to navigate to Google's authorization url, which contains a code parameter you’ll pass to your server assuming the user grants access to the requested permission scopes.

To handle the code parameter and log your user into their chat channel, you need to update App.jsx with the following code:

import React from "react";
import { StreamChat } from "stream-chat";
import { Chat } from "stream-chat-react";
import "./App.css"
import Cookies from "universal-cookie";

import Auth from "./components/Auth"
import ChannelContainer from "./components/ChannelContainer";

const client = StreamChat.getInstance(process.env.REACT_APP_STREAM_API_KEY);
const cookies = new Cookies();
const authToken = cookies.get('token')

if (authToken) {
   id: cookies.get('user_id'),
   name: cookies.get("name"),
 }, authToken);

const LogoutButton = ({ logout }) => (<button className="logout-button" onClick={logout}>Logout</button>);

const App = () => {
 const logout = () => {

 if (!authToken) return <Auth />

 return (
   <div className="app__wrapper">
     <Chat client={client}>
       <div className="sidebar">
       <LogoutButton logout={logout} />
         <ChannelContainer />

export default App;

In this snippet:

  • You retrieve your Stream token with cookies.get(‘token’) and declare the authToken variable.
  • Then, with connectUser(), you pass in the user_id, name, and authToken to create a websocket connection with the client.
  • You pass in the client object as a prop to your Chat context, which you’ll use in your ChannelContainer component to create a channel.
  • When the user clicks logout, you remove your cookies and close out the session.
  • Because you have an authToken, your app redirects the user to the chat dashboard inside ChannelContainer.

At this point, Google will redirect the user to the authorization url that you received in Auth.jsx with the scopes you defined in your server:

Permission scopes

Here, when your user grants permission to the scopes, they'll be redirected to your chat dashboard. If they deny access, they'll be sent back to the sign-in page.

The last component you need to handle is in ChannelContainer.jsx.

Add in the following code:

import React, { useEffect } from 'react';
import { Channel, Window, ChannelHeader, MessageList, MessageInput, Thread, useChatContext } from 'stream-chat-react';
import Cookies from 'universal-cookie';
import '../App.css';
import 'stream-chat-react/dist/css/index.css';

const cookies = new Cookies();

const ChannelContainer = () => {
   useEffect(() => {
           const params = new URLSearchParams(;
           let code = params.get("code");
           cookies.set("code", code);

           const URL = `http://localhost:3001/auth/googleauth`;
           fetch(URL, {
               method: 'POST',
               headers: {
               'Content-type': 'application/json'
               body: JSON.stringify({code: code, email: cookies.get("email")})
               }).then((res) => res.json()).then((gt) => {
                   const googleToken = gt.token;
                   cookies.set('googleToken', googleToken);

   const { client } = useChatContext();
   const { id } = client.user;
   const type = 'messaging';
   const channelName = 'business'
   const channel =, channelName, {
       name: channelName,
       members: [id]

   return (
           <Channel channel={channel}>
                   <ChannelHeader />
                   <MessageList />
                   <MessageInput />
               <Thread />

export default ChannelContainer;

In this snippet:

  • You import the stream-chat-react css file to give your channel a cleaner layout.
  • You use useChatContext() to access the client object, where you get your user’s id to create a channel.
  • Then, you pass channel as a prop into the Channel component.
  • In your useEffect hook, you parse the code parameter from the url your user lands on.
  • In your fetch request, you send your email and code to the /auth/googleauth endpoint.

To add a little bit more customization to your app’s look and feel (and to override some of Stream’s default css properties), add the following code to your App.css file:

* {
 box-sizing: border-box;
 text-align: center;
body {
 margin: 0;
 padding: 0;
 height: 100%;

.app__wrapper {
 display: flex;

/* Hooks button */
.button {
 cursor: pointer;
 display: block;
 font-size: 1.3em;
 box-sizing: content-box;
 margin: 20px auto 0px;
 width: 70%;
 padding: 15px 20px;
 border-radius: 24px;
 border-color: transparent;
 background-color: white;
 box-shadow: 0px 16px 60px rgba(78, 79, 114, 0.08);
 position: relative;

.sidebar {
 padding: 20px;
 width: 15%;
 --tw-bg-opacity: 1;
 background-color: rgba(0,95,255,var(--tw-bg-opacity));

.logout-button {
 height: 35px;
 border-radius: 20px;
 border: none;
 width: 135px;
 margin-top: 150px;
 --tw-bg-opacity: 1;
 background-color: white;
 color: black;
 cursor: pointer;
 font-weight: 700;

.buttonText {
 color: #4285f4;
 font-weight: bolder;

.icon {
 height: 25px;
 width: 25px;
 margin-right: 0px;
 position: absolute;
 left: 30px;
 align-items: center;

div {
box-sizing: border-box;
text-align: center;
width: 100%;

.str-chat__input-flat .rfu-file-upload-button {
 width: 1%;

span {
 white-space: nowrap;

li {
 text-align: left;

Handling Google’s Refresh and Access Tokens

A core part of OAuth 2.0 is refresh and access tokens. When using Google’s OAuth 2.0 flow, access tokens are required to make requests to Google APIs. This includes Google Calendar.

But, access tokens expire quickly, which means you need to generate a new one with your refresh token. Google handles this process automatically for you, but it’s on you as the developer to store your refresh token.

This next section will show you how:

In your controllers folder in auth.js, enter the following code:

const googleauth = async (req, res) => {
   try {
       const { email } = req.body;
       const {code} = req.body;

       const tokenRes = await dataModel.getRefreshToken(email);
       if(!tokenRes.refresh_token) {
           const r = await oAuth2Client.getToken(code);
           oAuth2Client.on('tokens', async (tokens) => {
               // On first authorization, store refresh_token
           if (tokens.refresh_token) {
               const refresh_token = tokens.refresh_token;
               dataModel.updateRefreshToken(refresh_token, email)


           return res.status(200).json({message: "User authorized"})

       res.status(200).json({message: "User authorized"});
   } catch (error) {

In this snippet, you receive the authorization code and email from your front-end, and then check if you’ve previously stored a refresh_token with dataModel.getRefreshToken(email).

If you have a refresh token, your server does nothing with the code. If you don’t have a refresh token, you pass in the code to oAuth2Client.getToken(), which will generate an access_token and refresh_token, but only on the first authorization.

On subsequent authorizations, Google will look for your stored refresh token to exchange for a new access token. To manage your tokens, use Google’s oAuth2Client.on() token event, which stores your refresh and access tokens for you (you still have to manually set your refresh token to get a new access token, but you’ll cover that in your webhook configuration).

After receiving your tokens, call setCredentials to authorize your user.

Configuring Your Webhook

Finally, now you can create a custom webhook.

First, fire up an ngrok process for your server:

$ ngrok https 3001

In your Stream dashboard:

  1. Click on your app.
  2. In the nav, select Chat, then Overview.
Stream dashboard overview
  1. Under the Realtime window, specify your url.
    • Note: This must be a publicly accessible URL. In addition to generating your URL with ngrok, you also need to add the /auth/handlewebhook endpoint.
Webhook url in Stream chat dashboard
  1. Select Save.

Now, in components/auth.js, add the following line of code to login under your serverClient instance:

await setupCommands(serverClient);

Then, create your setupCommands function to configure your custom commands webhook:

// Create webhook

const setupCommands = async (serverClient) => {
   try {
       const ngrokUrl = ``;
       const cmds = await serverClient.listCommands();

       if (!cmds.commands.find(({name}) => name === 'gcal')) {
           await serverClient.createCommand({
               name: "gcal",
               description: "Fetch your meetings for the day",
               args: "",

       const type = await serverClient.getChannelType('messaging');
       if (!type.commands.find(({name}) => name === 'gcal')) {
           await serverClient.updateChannelType('messaging', {commands: ['all', 'gcal']});

       // custom_action_handler_url has to be a publicly accessibly url
       await serverClient.updateAppSettings({custom_action_handler_url: ngrokUrl});

   } catch (err) {
       console.log(`Error setting up commands ${setupCommands}`);

In this snippet, you’re using your ngrokUrl for your custom_action_handler_url, and setting up the command so that Stream knows to look for the /gcal custom command.

Lastly, create your handlewebhook handler:

const handlewebhook = async (req, res) => {
   const { user, message, form_data } = req.body;
   const user_id =;

   const rToken = await dataModel.getRefreshTokenWithId(user_id).then((data) => {
       return data

   oAuth2Client.setCredentials({refresh_token: rToken.refresh_token});
   const calendar = google.calendar({version: 'v3', oAuth2Client});
   const response = await{
       calendarId: 'primary',
       timeMin: (new Date()).toISOString(),
       maxResults: 5,
       singleEvents: true,
       timeMin: (new Date()).toISOString(),
       orderBy: 'startTime',

       const r = await response;
       const events =;
       const list = => {

           const hStart = event.start.dateTime.substr(11, 2);
           const hEnd = event.end.dateTime.substr(11, 2);

           const mStart = event.start.dateTime.substr(14, 2);
           const mEnd = event.end.dateTime.substr(14, 2);

           const hStartNum = parseInt(hStart);
           const hEndNum = parseInt(hEnd);

           const hoursStart = ((hStartNum + 11) % 12 + 1);
           const hoursEnd = ((hEndNum + 11) % 12 + 1);


           const startString = hoursStart + ":" + mStart + " ";
           const endString = hoursEnd + ":" + mEnd + " ";

           const summary = event.summary.trim();
           const eventString = `\n- ` + startString +` - `+ endString + ` : ` + summary;
          return eventString
       message.text = ''; // remove user input
       message.mml = `<mml><md>Here are your events:

   return res.status(200).json({ ...req.body, message });


In this snippet:

  1. You get your user_id and the message (/gcal) from your request and fetch your user’s refresh token with getRefreshTokenWithId().
  2. Using your global oAuthClient2 instance, you call setCredentials({refresh_token: rToken.refresh_token}) to get a new access token.
    • Note: This part is crucial. Google will automatically replace your expired access token with a new one, but you must call setCredentials with the {refresh_token} object here to do so. If you don’t, your access token will expire, which will force the user to log in again to get new access and refresh tokens.
  3. Next, you call google.calendar() and pass in your oAuth2Client to make a call to the Google Calendar API to retrieve your events.

You’ll get a list of the next five events from your Google Calendar. Using some JavaScript, you can then extract start and end times from your Date strings.

Lastly, using Stream’s Message Markup Language (MML), which is currently only supported in the React SDK, you can create a list of your events using MarkDown and send it back to your frontend client.

The end result will look like this:

Wrapping Up

Congratulations! In this tutorial, you successfully implemented Google’s OAuth 2.0 flow using refresh and access tokens to make API calls to retrieve events from Google Calendar.

You also configured your own webhooks in your server and in Stream’s dashboard, enabling you to implement your own custom commands.

Plus, you built the beginnings of a full-stack application that uses a SQL database and some basic SQL queries.

You can find all the code for the front-end project in this GitHub repo, and the code for the backend project in this GitHub repo.

As always, show us what you're working on @getstream_io and keep coding!