In this tutorial, we follow the adventures of Captain Webhook and his crew in their insatiable desire to report their latest booty conquest to me via Google Chat webhooks…
That, dear readers, did not come out right.
Webhooks are HTTP POST requests that are usually generated by a triggered event. The event could come from any third-party source within Google Workspace like:
- When a user submits a form or clicks a button on a WebApp.
- A Google Form is Submitted.
- An onEdit() event in Google Sheets like a check box is ticked.
- An hourly clock trigger event runs a script and returns some values.
- An error triggers the Google Cloud monitoring service.
- When a specific email type comes in.
Or it can occur with other third-party services. Imagine that you want to get an update from:
- Your Stripe or PayPal account whenever a new payment comes in.
- A course management service like Gumroad.
- Patreon when another awesome supporter shows you some love.
Or when a rather rambunctious figment of my imagination insists on updating me when his latest haul of treasure has come in… live.
Sigh.
Chat App webhooks will need an intermediary step for them to be compiled in a way that the Google Chat API can understand. We’ve chosen Google Apps Script here to do this, but you can choose to use any other language to convert your data into Chat API readable JSON or even build in a CLI to post your webhook request.
In this tutorial, we will cover how to:
Table of Contents
The Starter Google Apps Script Project
Grab your copy of the starter Google Apps Script project below so that you can play along. It’s got some extra code snippets in there to save you some time during the tutorial.
Besides, you’ll miss out on the fun if you just read along.
Once open, select Project details from the sidebar and then select the copy icon.
The Video Tutorial
Set up your Google Chat Space to receive a webhook.
Head over to either Gmail or Google Chat and create a new Space. Alternatively, you can add the webhook to an existing space too.
I’ll be working directly in Google Chat for this example.
Create a new Google Chat Space
In Google Chat, open:
- Open the sidebar.
- Select the plus button next to the Spaces (or even the ‘Chat’ section).
- Select Create space.
A dialogue box with appear with options to create a new Google Chat Space.
- Select a memorable icon or image.
- Add a title.
- Describe the Space for the benefit of the users.
- Add your users to the Space.
- Set up who can access the Space.
- Select Create.
Create a URL to the Chat Space
You will now need to create a URL to the Space. This will contain a unique key and token that your webhook will use to access the space.
- The title of the Space has a dropdown icon to the right of it. Select it.
- Then select Apps and Integration.
This will open the Apps and Integration page.
- Scroll to the bottom where it says Webhooks.
- Select the Add webhooks button.
- Add a title for the webhook. In our example, we will use “Booty Reports”.
- Add an avatar to represent the incoming messages for your webhook. Add the following link for this:
https://yagisanatode.com/wp-content/uploads/2023/10/Webhook-Yarh.png
- Select Save.
This will generate a custom webhook link containing a unique token and key.
To get a copy of the link:
- Click the 3 vertical ellipses to the right of the new webhook.
- Select copy.
Your webhook URL should look a little like this:
1 2 |
// https://chat.googleapis.com/v1/spaces/AAAAoNh92rI/messages?key=<YOUR_KEY>&token=<YOUR_TOKEN> |
Make sure that you keep your key and token a secret. This can be used by anyone to submit messages to your Google Chat Space.
Create your first Google Chat App Webhook
Now that you have your delightful little copy of your webhook URL it is time to head over to your copy of the Starter Apps Script Project.
We’re going to add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//Sends a text message to a Google Chat Space. function webhook_v1() { const url = "https://chat.googleapis.com/v1/spaces/AAAA6cYPowQ/messages?key=<YOUR_KEY>&token=<YOUR_TOKEN>"; const options = { "method": "post", "headers": { "Content-Type": "application/json; charset=UTF-8" }, "payload": JSON.stringify({ "text": "I'm hooked! Yarh!!! 🏴☠️" }) }; const response = UrlFetchApp.fetch(url, options); console.log(response.getContentText()) } |
Here we are using Google Apps Script UrlFetchApp class fetch method to make a post request to our wehook URL.
As you can see on line 12 the fetch
method can take two parameters:
- The URL to request.
- A list of options that include the method type, header info and possibly a payload of data to send.
Line 2: Sets the webhook URL as our URL
variable.
Lines 3: Creates the options constant variable that will be added to the second parameter of the fetch method.
Line 4: Here we must declare the URL request type as ‘POST’ to indicate that we are sending data.
Line 5 – 7: Next, for the header data we will need to set our content type to JSON. This is how the Chat API will receive the data and convert it into a message.
Lines 8 – 10: We then set the payload of data that we want to send to the Chat API.
Chat App data needs to be converted into JSON format for the Chat API parse. This payload data has a limited set of commands that you can use to send text or even a stylised card to the Google Chat Space.
In the example above, we are sending a simple text to the Chat Space.
Line 12: As mentioned above, this is the stage in which we execute the fetch request. We will store the results in the response constant
variable.
Line 13: While not entirely necessary, it will be helpful for us to later see what the Google Chat API returns when the message is sent successfully.
Note! While I would normally include a way to handle a bad connection, I have omitted this from this tutorial to focus on sending messages. One simple option for handling errors is to use the muteHttpExceptions
property set to true in the header. This will not throw an error but record it for you to either try again or handle in future. This is, in my opinion, a more elegant option to using a JavaScript Try…Catch Statement.
Running your Webhook
The power of incorporating webhooks into a Google Chat Space is that you can receive regular updates or updates triggered on events right in chat.
For now, lest just run our code to trigger our webhook post request. You, I and… sigh… Captian Webhook’s crew will simulate a more useful (Questionable) event in our last activity in this tutorial.
In your Google Apps Script IDE, select the webhook_v1
you created and of course did not just copy and paste in, and run the code.
The first time through you will need to run through the authorisation process.
Running Google Apps Script for the First Time: What’s with all the Warnings!
You then may need to run the function again.
On a successful run, you should see the following JSON string returned from your Chat API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// Google Chat App Webhook text response example { "name": "spaces/AAAAoNh92rI/messages/QVN9inpNSvQ.QVN9inpNSvQ", "sender": { "name": "users/4444444333332222211111", "displayName": "Booty Reports", "type": "BOT" }, "createTime": "2023-10-11T02:34:20.245727Z", "text": "I'm hooked! Yarh!!! 🏴\u200d☠️", "thread": { "name": "spaces/AAAAoNh92rI/threads/QVN9inpNSvQ", "retentionSettings": { "state": "PERMANENT" } }, "space": { "name": "spaces/AAAAoNh92rI", "type": "ROOM", "displayName": "Captain Webhook and Crew's booty calls.", "spaceThreadingState": "THREADED_MESSAGES", "spaceType": "SPACE", "spaceDetails": { "description": "An exhaustive interruption regarding the lasted loot and treasure hauled in from the crew of the HTTP Request." }, "spaceHistoryState": "HISTORY_ON" }, "argumentText": "I'm hooked! Yarh!!! 🏴\u200d☠️", "retentionSettings": { "state": "PERMANENT" }, "formattedText": "I'm hooked! Yarh!!! 🏴\u200d☠️" } |
And of course, the most important part, the result, in the Chat Space:
Sending your Webhook Response to a Google Chat Reply
In Google Chat and Spaces we can directly reply to a message and subsequently generate a thread. This is helpful so that we don’t overburden the main thread in the chat.
Replying to a message. Create a thread.
Let’s give this a crack manually so we can see what’s going on.
Head to the space where your webhook message landed and hover over the message. You will see a little dialogue appear. Click the ‘Reply in thread’ icon.
A thread window will appear to the right of the main messages.
- Reply to Captain Webhook. He is thirsty for validation.
- Take a look at the main thread. You will see the reply count increase by one.
So how do we send our webhook as a reply rather than spamming the main thread?
We can do this by generating a thread key the first time that we send our message and then referencing that thread key to reply within the thread.
Creating a Webhook Message with a Thread Key
Captain Webooks wants to send us a message each time he and his crew go on a raid. When he sends his initial message, he will generate a thread key.
When each crew member comes back with some booty they will report the treasure as a reply in the thread to keep everything in one place for that particular raid.
Let’s see how we can update the script to reply to a thread only.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// // Sends a Google Chat Message as a webhook containing a Thread Key // function webhook_v2() { const url = "https://chat.googleapis.com/v1/spaces/AAAA6cYPowQ/messages?"+ "messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD&"+ "thread_key=booty001&"+ "key=<YOUR_KEY>&"+ "token=<YOUR_TOKEN>" const options = { "method": "post", "headers": { "Content-Type": "application/json; charset=UTF-8" }, "payload": JSON.stringify({ "text": "Pirate raid! Show me the BOOTY!!! 💎👛⚔🏴☠️🦜", }) }; const response = UrlFetchApp.fetch(url, options); console.log(response.getContentText()) } |
In this example, we have separated the URL constant into lines for ease of reading. The first line is the URL to the Google Chat Space and then the following lines are for each query.
Line 7: Our first query sets if and how we should reply to a message with the Google Chat App API messageReplyOption
property. There are three possible options here:
MESSAGE_REPLY_OPTION_UNSPECIFIED
: This is the default setting and if not included in the query, will be implied. It means that the webhook will create a new thread when posted.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD
: This is used to create a new thread in conjunction with either a thread ID or a thread key. If a message with a matching thread key or ID exists, then the message will be a reply to the existing thread otherwise it will generate a new thread with the set key or the automatically generated thread ID.REPLY_MESSAGE_OR_FAIL
: Similar to the previous option, this option does not generate a new thread if the current one does not exist. It will fail and return an error if no matching key or ID is found.
When creating a new thread key it might be helpful for us to use option two here just in case it already exists.
Line 8: Here we set the thread key with the thread_key
property. For this example, we have set the thread key to ‘booty001’ presuming that there will be other booties in the future.
Note: At the time of writing this there has been some ongoing confusion in the documentation regarding the correct wording of the thread_key
property. The previous threadKey
property has since been depreciated. However, the documentation still displays some mixed properties over its docs.
Do the Google Devs a solid and write some feedback for them to let them know as I have.
Create a new thread in Google Chat – how do you send a new specified threadkey?
https://stackoverflow.com/a/58419860/13226950
Using Google Chat webhooks to place a message inside a thread: https://stackoverflow.com/a/75836028/13226950
Lines 9 & 10: Here again we add our private key and token to access the Google Chat Space with our webhook.
Everything else is essentially the same as before.
Go ahead and run webhook_v2
a few times. You will see that on the first run, it will create a new message. You will also be able to see in the logs that a thread key has been added.
However, on consecutive runs, the messages will be added as responses to the original with the same thread key.
Adding the thread key in the payload instead
An interesting alternative that I found while researching for this post is that we can also add the thread_key
property to the JSON payload instead with the same result.
Check it out!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// //Creating a Google Chat Webhook text message with a thread key an alternate approach // function webhook_v3() { const urlAll = "https://chat.googleapis.com/v1/spaces/AAAAoNh92rI/messages?"+ "messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD&"+ "key=&"+ "token=" const options = { "method": "post", "headers": { "Content-Type": "application/json; charset=UTF-8" }, "payload": JSON.stringify({ "text": "Booty thread be from object, yarh!", "thread": { "thread_key": "booty001" } }) }; const response = UrlFetchApp.fetch(urlAll, options); console.log(response.getContentText()) } |
Note that we still need to include the messageReplyOption
in the query.
Lines 17-19: Here we can see that this approach resembles the returned JSON object from the HTTP request. We must first call the ‘thread’ property before setting the ‘thread_key’ here.
Go ahead and give webhook_v3
a run with a little different text so that you can see it included in the current booty001
reply.
Sending a Webhook as a Formatted Card
I know, based on my previous work using the Google Chat API that I can create stylised cards with images, buttons and fancy fonts.
You can learn more about this here:
Google Chat Apps for Google Apps Script Developers (updated 2023-11-04)
Seeing the layout of the HTTP response and how the payload is set up, I was curious to see if I could implement cards via webhook too.
Well, guess what? I can.
Now Captain Webhook and his crew can share booty pic… wait … treasure pictures that they retrieve from their latest raid.
Adding the Card Property to the Payload
Let’s create another webhook function that will add our card to the main thread:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// //Creating a Google Chat Webhook Card in the main thread // function webhook_v4() { const booty = getBooty() const pirate = getPirate() const url = "https://chat.googleapis.com/v1/spaces/AAAAoNh92rI/messages?"+ "key=<YOUR_KEY>&"+ "token=<YOUR_TOKEN>"; const options = { "method": "post", "headers": { "Content-Type": "application/json; charset=UTF-8" }, "payload": JSON.stringify({ "text": "We found some booty Cap'n!", "cardsV2": createCard(booty, pirate), }) }; const response = UrlFetchApp.fetch(url, options); console.log(response.getContentText()) } |
Adding some dummy pirate raid data
Lines 6 & 7: First, we will add some dummy data to display on our card.
In the starter sheet I have created two functions to do this:
getBooty()
: This retrieves a random booty type as an object containing the title and a URL to the image of the objects. Imagine the crew taking a photo of each haul as they retrieve it and updating the captain.
The object looks like this:{type, url}
.getPirate()
: Similarly, this function retrieves a random pirate by name.
UPDATE the payload
Line 20: Here you can see that we have updated the payload with some text. You don’t need to have both text and a card together, but I have added it here as a reference point for you.
Line 21: Here we set the “cardsV2” property. This will take an object containing the layout of the card in the same format as the Chat API Cards V2 structure.
We will create a separate function to generate our card object for ease of reading.
Create a function called createCard
. This function will take our random booty and random pirate data and return the Google Chat V2 card object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// /** * Creates a card to display the booty from the current haul. * @param {Object} booty - {type, url} * @param {String} pirate - the pirate's name * @returns {Object} the Cards V2 object for the webhook payload. */ function createCard(booty, pirate){ const cardHeader = { title: `BOOTY TYPE: ${booty.type}` }; const bootyWidget = { "decoratedText": { "topLabel":"PIRATED BY:", "startIcon": { "knownIcon": "PERSON" }, "text": `<span style="color: #5e2a0e;">${pirate}</span>` } }; const bootyImageWidget = { "image": {imageUrl: booty.url} }; const bootySection = { widgets: [ bootyWidget, bootyImageWidget ], }; return [{ "cardId": "booty", "card": { "name": 'Yerh booty, Cap\'n!', "header": cardHeader, "section"s: [bootySection], } }] } |
This sort of builder function is best reviewed in reverse order. Let’s start from the bottom.
The Card
Line 34: This encapsulates the main Card V2 object as an array of cards.
Line 35: Each card can have its own unique id.
Line 36 – 40: The card
property contains a child object containing properties about the card. Each card contains a human-readable name for the card, a header object for the card and an array of sections found within the card.
The Header
Lines 9 – 11: The header object can contain a number of styling properties like title, subtitle and an icon image. For us, we are just adding a title.
The Section
Lines 27 – 32: While you can have multiple sections in a card, we have chosen just one in our example.
Each section contains an array of widgets.
For us, the bootySection
object contains two widgets: one to handle some decorated text and one to display an image of the treasure retrieved.
The Widgets
Lines 13 – 21: Here we have used the decorated text widget to create a label describing the purpose of the widget (who pirated the object) and then the name of the pirate and a helpful icon of a person.
Sadly, Google Chat cards do not have a standard known icon for ‘PIRATE’. This should really be addressed!
As you can see decorated text widgets allow us to add extra information, buttons and images into the card. We can even do some minor styling of the text to colour it or add a font style.
Lines 23 – 25: Finally, we add an image of the treasure to the card via an image widget.
You can learn more about creating Chat App Cards here:
https://yagisanatode.com/google-chat-apps-for-google-apps-script-developers/#Cards_and_Dialogues
Running the Chat App Card Webhook
With both functions created, go ahead and run the webhook_v4
function.
You should get something like this:
Give it a few runs to see what booty turns up.
Also, note in your logs that the card object is returned to you too.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// // Logged Google Chat App Webhook response with Card Properties // ... "argumentText": "We found some booty Cap'n!", "cardsV2": [ { "cardId": "booty", "card": { "header": { "title": "BOOTY TYPE: Textiles" }, "sections": [ { "widgets": [ { "decoratedText": { "topLabel": "PIRATED BY:", "text": "\u003cfont color=\"#5e2a0e\"\u003eJellylegs Jenkins the Jittery\u003c/font\u003e", "startIcon": { "knownIcon": "PERSON" } } }, { "image": { "imageUrl": "https://yagisanatode.com/wp-content/uploads/2023/10/Pirate_Textiles.png" } } ] } ], "name": "Yerh booty, Cap'n!" } } ], ... |
Time for a fun demo.
Running Captain Webhooks Booty Haul – A Demo
Okay. Let’s see this in action. Here’s the setup.
- Each time Captain Webhook conducts a raid with his crew a new unique thread key is generated.
- First, a post on the original thread is generated to indicate a raid.
- Then as the crew find treasure, they report the treasure along with a pic of the booty until there is no more to be pillaged.
Add the following code to your project and update the URL, key and token in the secret
variable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// // Captain Webhooks Pirate Raid Google Chat App Webhook // const secret = { url: "", key: "", token: "" } function notifyHaul_webhook(haulId){ console.log(haulId) const url = secret.url + "?"+ "messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD&"+ "thread_key="+haulId+"&"+ "key="+ secret.key +"&"+ "token=" + secret.token const options = { "method": "post", "headers": { "Content-Type": "application/json; charset=UTF-8" }, "payload": JSON.stringify({ "text": "Pirate raid! Show me the BOOTY!!! 💎👛⚔🏴☠️🦜", }) }; const response = UrlFetchApp.fetch(url, options); const respText = response.getContentText() console.log(respText) const threadKey = JSON.parse(respText).thread.threadKey return threadKey } function haulBooty_webhook(threadKey, booty, pirate) { const url = secret.url + "?"+ "messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD&"+ "key="+ secret.key +"&"+ "token=" + secret.token const options = { "method": "post", "headers": { "Content-Type": "application/json; charset=UTF-8" }, "payload": JSON.stringify({ "text": "We found some booty Cap'n!", "cardsV2": createCard(booty, pirate), "thread": { "thread_key": threadKey // Add in hread section. } }) }; const response = UrlFetchApp.fetch(url, options); console.log(response.getContentText()) } function startRaid(){ createRaid() } |
Then run startRaid()
to begin the raid and quickly flip over to your Chat App console and see the raid in progress.
Go ahead and run another raid and see what happens.
Behind the scenes, I have created a call to create the initial raid with a unique thread key each time a raid occurs.
After that randomly scheduled events will trigger the retrieval of a random booty item by a random crew member. This will, in turn, update the Chat ten times.
Pretty fun, hey?
Conclusion
Hopefully, from the final example, you can see the potential for what webhooks can achieve in Google Chat.
Personally, I have set up a monitoring service for error logging for apps that I am responsible for. This way I can see what is happening live and address it quickly.
I also get updates from my “Hire Me” pages for new potential clients and live updates from “Stripe” when a sale comes through for one of my courses or products right in my Google Chat. You know, do the Scrooge McDuck dive into the money vault thing.
I would love to hear how you would implement webhook for Google Chat in the comments. It is always amazing to see where people go with these tutorials.
And, if you want to learn more about creating Google Chat Apps in Google Apps Script, check out my free course below on creating a Currency Converter App:
Develop a Google Chat App Currency Converter with Google Apps Script
Need help with Google Workspace development?
Go something to solve bigger than Chat GPT?
I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
Schedule a free consultation today to discuss your needs and get started or learn more about our services here.
~ Yagi

great job Yagi, thank you as usual. a little question: if I try to uso the ‘Message with a Thread Key’ way (i.e. reply to an existing thread instead of spamming the main one) I don’t get toast notification on Android devices. I had a look to all settings about notifications (Google Chat settings as well as Android system and app related settings), it all seems ok. Descriptions on Google Chat settings suggest that both new threads and replies should generate notifications… do you know anything about a secret setting I’d look for? or maybe a bug? thank you
Ye
Good find. After some testing I had no luck either. Notification setting were supposed to include threads for the top option. I’ve sent a bug report in the feedback. The more of us who do the same the more likely they will address the issue.
thank you again. I’ll do it. If you opened a bug report on issuetracker maybe if you share the issue id/link we can star it instead of creating new ones. otherwise I’ll do it on my own.
I created an in-app bug report this time. I’m on my phone today.