Google Chat incoming webhooks: post to a space and test the request
A Google Chat incoming webhook is a URL that posts messages into one space: send it a POST with JSON such as a text field. To see the exact request your code sends before it hits the space, point it at a free webhooks.sh bin: no signup, 48 hours, 100 requests a day.
A Google Chat incoming webhook is a secret URL tied to one space, and you POST JSON to it to drop a message in. The quickest way to get the request right, including the threading parameters, is to send it to a free bin first and read the exact payload your code sent, then switch to the real URL. This page covers setup, the URL parts, text and threads, limits, and what webhooks can't do.
Create the webhook in a space
To set up a Google Chat webhook, you need a Business or Enterprise Google Workspace account and an organization that allows incoming webhooks (if you can't click the Add webhooks button, an admin has turned it off).
- Open Google Chat in a browser, not the mobile app.
- Go to the space where you want to post messages.
- Click the arrow next to the space title and choose
Apps & integrations. - Click
Add webhooks. - Enter a name and an optional avatar URL, then click
Save. - Click
More>Copy link.
The URL you copy looks like https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN. SPACE_ID identifies that specific space, and key is a common value shared across webhooks. The token parameter is unique to this webhook, so you have to keep it secret and keep the full URL out of public code repositories.
Preview the request in a free bin first
Before your script posts to a live space, create a free bin at view.webhooks.page or with the command below. Swap the Google Chat URL in your code for your bin's ingest URL (https://<bin>.webhooks.page/), keep the same path and the messageReplyOption parameter, and send the request your code would send. The viewer shows the method, query string, Content-Type header and JSON body as they arrive; when everything looks right, put the Chat URL back.
A free bin always replies with HTTP 200 and its own small JSON ({"ok":true,"delivery_id":"..."}), while the Google Chat API replies with a Message object where only the name and thread.name fields are populated, so any code that reads the reply needs one real post to the space to test.
# 1. Create a free bin (or open view.webhooks.page)
curl -X POST https://webhooks.sh/v1/bins
# 2. Send the exact request your script will send to Google Chat
curl -X POST "https://<bin>.webhooks.page/v1/spaces/SPACE_ID/messages?messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" \
-H "Content-Type: application/json; charset=UTF-8" \
-d '{"text": "*Disk alert* on db-1: 91% used", "thread": {"threadKey": "disk-db-1"}}'
Text messages and threads
The simplest payload is a JSON object with a text field. Webhook messages default to Chat markup syntax (MARKUP_SYNTAX_CHAT), which means bold uses a single asterisk like *bold*, and custom emojis don't work in messages created by a webhook.
To group related alerts, add thread.threadKey to the JSON body: sending a new string starts a thread, and sending that same string again replies inside it. You also need to add messageReplyOption to the webhook URL to tell Chat what to do when that key isn't found. Setting messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD replies if the thread exists or starts a new thread if it doesn't, while messageReplyOption=REPLY_MESSAGE_OR_FAIL won't send the message at all if the key isn't found.
Google's webhook quickstart shows basic text bodies and links out to its card message guide if you want to build cards. Any text and cards you send count together toward the 32,000-byte maximum message size, so longer output has to be split across several messages.
import json
import urllib.request
WEBHOOK_URL = "https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"
def post_alert(text: str, thread_key: str) -> dict:
body = json.dumps({"text": text, "thread": {"threadKey": thread_key}}).encode()
request = urllib.request.Request(
WEBHOOK_URL,
data=body,
headers={"Content-Type": "application/json; charset=UTF-8"},
method="POST",
)
with urllib.request.urlopen(request) as response:
return json.load(response)
Limits that make posts fail
Most failed posts come down to hitting the per-space rate limit or the payload size limit.
You should queue or batch your alerts so a single space never gets more than 1 request per second across all of its webhooks. When you do hit a quota, time-based, or network error, retry the request with exponential backoff, and make sure your retry logic branches on the HTTP error code (such as 429) rather than parsing the error message text, since Google says the message text can change.
| Limit | Value | What to do |
|---|---|---|
| Requests per space | 1 per second, shared by every webhook in the space | Queue or batch alerts |
| Over the quota | HTTP 429 Too many requests | Retry with exponential backoff |
| Message size | 32,000 bytes, text and cards together | Split long output into several messages |
| Spaces per webhook | 1, the space it was created in | Create one webhook per space |
| Reply from Chat | A Message with only name and thread.name | Use threadKey to reply, not the response |
What a webhook can't do
- It can't read or answer messages from people or Chat app interaction events, since it's strictly one-way.
- It can't be shared to other spaces or published to the Google Workspace Marketplace.
- It works in direct messages only when every user in the conversation has Chat apps enabled.
- It requires a Business or Enterprise Google Workspace account.
If you need to receive or respond to messages from users, build a Chat app instead.
Keep the URL for longer
A free bin lasts 48 hours and takes up to 100 requests a day, plenty to check alert formats and threading. To keep the same URL, claim the bin with a free Google sign-in before it expires; the timer stops and new requests are kept for 30 days on a rolling basis.
For a service that receives webhooks, Solo ($12 a month, see pricing) gives you named endpoints on webhooks.host with 250,000 requests per billing period, plus the wsh CLI to relay live requests straight to localhost (wsh listen <endpoint> --to localhost:3000). If you're also wiring up alerts for other chat tools, see our Microsoft Teams webhooks guide for the Teams equivalent.
FAQ
How do I create a Google Chat webhook?
Open Google Chat in a web browser, click the arrow next to the space name, and select Apps & integrations. Click Add webhooks, enter a name and optional avatar URL, click Save, and then click More > Copy link to get the URL.
What is the Google Chat webhook rate limit?
1 request per second per space, shared by every webhook in that space. Going over that quota returns HTTP 429 (Too many requests) errors, which you should retry with exponential backoff.
How do I reply in a thread with a Google Chat webhook?
Pass thread.threadKey in your JSON body using the same string for every message that belongs in the thread, and add messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD to the webhook URL.
Can a Google Chat webhook receive messages?
No, incoming webhooks are one-way and only post messages into a space. To receive or answer messages from users, you have to build a Chat app.
Why can't I add a webhook in Google Chat?
You need a Business or Enterprise Google Workspace account, and your Workspace admin has to allow incoming webhooks (if the Add webhooks button isn't clickable, your organization has it turned off). You also have to set it up in a web browser, because webhooks can't be configured from the Google Chat mobile app.
Is a Google Chat webhook URL secret?
Yes. The token parameter in the URL is unique to that webhook and Google says to keep it secret, so don't commit the URL to a public code repository or share it online.
Last updated
Try it in 10 seconds.
Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.