Google Chat API
Status: Production Ready
Last Updated: 2026-01-09 (Added: Spaces API, Members API, Reactions API, Rate Limits)
Dependencies: Cloudflare Workers (recommended), Web Crypto API for token verification
Latest Versions: Google Chat API v1 (stable), Cards v2 (Cards v1 deprecated), wrangler@4.54.0
Quick Start (5 Minutes)
1. Create Webhook (Simplest Approach)
bash
# No code needed - just configure in Google Chat
# 1. Go to Google Cloud Console
# 2. Create new project or select existing
# 3. Enable Google Chat API
# 4. Configure Chat app with webhook URL
Webhook URL:
https://your-worker.workers.dev/webhook
Why this matters:
- Simplest way to send messages to Chat
- No authentication required for incoming webhooks
- Perfect for notifications from external systems
- Limited to sending messages (no interactive responses)
2. Create Interactive Bot (Cloudflare Worker)
typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const event = await request.json()
// Respond with a card
return Response.json({
text: "Hello from bot!",
cardsV2: [{
cardId: "unique-card-1",
card: {
header: { title: "Welcome" },
sections: [{
widgets: [{
textParagraph: { text: "Click the button below" }
}, {
buttonList: {
buttons: [{
text: "Click me",
onClick: {
action: {
function: "handleClick",
parameters: [{ key: "data", value: "test" }]
}
}
}]
}
}]
}]
}
}]
})
}
}
CRITICAL:
- Must respond within timeout (typically 30 seconds)
- Always return valid JSON with array
- Card schema must be exact - one wrong field breaks the whole card
3. Verify Bearer Tokens (Production Security)
typescript
async function verifyToken(token: string): Promise<boolean> {
// Verify token is signed by chat@system.gserviceaccount.com
// See templates/bearer-token-verify.ts for full implementation
return true
}
Why this matters:
- Prevents unauthorized access to your bot
- Required for HTTP endpoints (not webhooks)
- Uses Web Crypto API (Cloudflare Workers compatible)
The 3-Step Setup Process
Step 1: Choose Integration Type
Option A: Incoming Webhook (Notifications Only)
Best for:
- CI/CD notifications
- Alert systems
- One-way communication
- External service → Chat
Setup:
- Create Chat space
- Configure incoming webhook in Space settings
- POST JSON to webhook URL
No code required - just HTTP POST:
bash
curl -X POST 'https://chat.googleapis.com/v1/spaces/.../messages?key=...' \
-H 'Content-Type: application/json' \
-d '{"text": "Hello from webhook!"}'
Option B: HTTP Endpoint Bot (Interactive)
Best for:
- Interactive forms
- Button-based workflows
- User input collection
- Chat → Your service → Chat
Setup:
- Create Google Cloud project
- Enable Chat API
- Configure Chat app with HTTP endpoint
- Deploy Cloudflare Worker
- Handle events and respond with cards
Requires code - see
templates/interactive-bot.ts
Step 2: Design Cards (If Using Interactive Bot)
IMPORTANT: Use Cards v2 only. Cards v1 was deprecated in 2025. Cards v2 matches Material Design on web (faster rendering, better aesthetics).
Cards v2 structure:
json
{
"cardsV2": [{
"cardId": "unique-id",
"card": {
"header": {
"title": "Card Title",
"subtitle": "Optional subtitle",
"imageUrl": "https://..."
},
"sections": [{
"header": "Section 1",
"widgets": [
{ "textParagraph": { "text": "Some text" } },
{ "buttonList": { "buttons": [...] } }
]
}]
}
}]
}
Widget Types:
- - Text content
- - Buttons (text or icon)
- - Text input field
- - Dropdowns, checkboxes, switches
- - Date/time selection
- - Horizontal line
- - Images
- - Text with icon/button
Text Formatting (NEW: Sept 2025 - GA):
Cards v2 supports both HTML and Markdown formatting:
typescript
// HTML formatting (traditional)
{
textParagraph: {
text: "This is <b>bold</b> and <i>italic</i> text with <font color='#ea9999'>color</font>"
}
}
// Markdown formatting (NEW - better for AI agents)
{
textParagraph: {
text: "This is **bold** and *italic* text\n\n- Bullet list\n- Second item\n\n```\ncode block\n```"
}
}
Supported Markdown (text messages and cards):
- or
- for inline code
- or for lists
- for multi-line code
Supported HTML (cards only):
- , ,
<font color="#FF0000">colored</font>
Why Markdown matters: LLMs naturally output Markdown. Before Sept 2025, you had to convert Markdown→HTML. Now you can pass Markdown directly to Chat.
CRITICAL:
- Max 100 widgets per card - silently truncated if exceeded
- Widget order matters - displayed top to bottom
- cardId must be unique - use timestamp or UUID
Step 3: Handle User Interactions
When user clicks button or submits form:
typescript
export default {
async fetch(request: Request): Promise<Response> {
const event = await request.json()
// Check event type
if (event.type === 'MESSAGE') {
// User sent message
return handleMessage(event)
}
if (event.type === 'CARD_CLICKED') {
// User clicked button
const action = event.action.actionMethodName
const params = event.action.parameters
if (action === 'submitForm') {
return handleFormSubmission(event)
}
}
return Response.json({ text: "Unknown event" })
}
}
Event Types:
- - Bot added to space
- - Bot removed
- - User sent message
- - User clicked button/submitted form
Critical Rules
Always Do
✅ Return valid JSON with
array structure
✅ Set unique
for each card
✅ Verify bearer tokens for HTTP endpoints (production)
✅ Handle all event types (MESSAGE, CARD_CLICKED, etc.)
✅ Keep widget count under 100 per card
✅ Validate form inputs server-side
Never Do
❌ Store secrets in code (use Cloudflare Workers secrets)
❌ Exceed 100 widgets per card (silently fails)
❌ Return malformed JSON (breaks entire message)
❌ Skip bearer token verification (security risk)
❌ Trust client-side validation only (validate server-side)
❌ Use synchronous blocking operations (timeout risk)
Known Issues Prevention
This skill prevents 6 documented issues:
Issue #1: Bearer Token Verification Fails (401)
Error: "Unauthorized" or "Invalid credentials"
Source: Google Chat API Documentation
Why It Happens: Token not verified or wrong verification method
Prevention: Template includes Web Crypto API verification (Cloudflare Workers compatible)
Issue #2: Invalid Card JSON Schema (400)
Error: "Invalid JSON payload" or "Unknown field"
Source: Cards v2 API Reference
Why It Happens: Typo in field name, wrong nesting, or extra fields
Prevention: Use
library or templates with exact schema
Issue #3: Widget Limit Exceeded (Silent Failure)
Error: No error - widgets beyond 100 simply don't render
Source: Google Chat API Limits
Why It Happens: Adding too many widgets to single card
Prevention: Skill documents 100 widget limit + pagination patterns
Issue #4: Form Validation Error Format Wrong
Error: Form doesn't show validation errors to user
Source: Interactive Cards Documentation
Why It Happens: Wrong error response format
Prevention: Templates include correct error format:
json
{
"actionResponse": {
"type": "DIALOG",
"dialogAction": {
"actionStatus": {
"statusCode": "INVALID_ARGUMENT",
"userFacingMessage": "Email is required"
}
}
}
}
Issue #5: Webhook "Unable to Connect" Error
Error: Chat shows "Unable to connect to bot"
Source: Webhook Setup Guide
Why It Happens: URL not publicly accessible, timeout, or wrong response format
Prevention: Skill includes timeout handling + response format validation
Issue #6: Rate Limit Exceeded (429)
Error: "RESOURCE_EXHAUSTED" or 429 status code
Source: Google Chat API Quotas
Why It Happens: Exceeding per-project, per-space, or per-user request limits
Prevention: Skill documents rate limits + exponential backoff pattern
Configuration Files Reference
Cloudflare Worker (wrangler.jsonc)
jsonc
{
"name": "google-chat-bot",
"main": "src/index.ts",
"compatibility_date": "2026-01-03",
"compatibility_flags": ["nodejs_compat"],
// Secrets (set with: wrangler secret put CHAT_BOT_TOKEN)
"vars": {
"ALLOWED_SPACES": "spaces/SPACE_ID_1,spaces/SPACE_ID_2"
}
}
Why these settings:
- - Required for Web Crypto API (token verification)
- Secrets stored securely (not in code)
- Environment variables for configuration
Common Patterns
Pattern 1: Notification Bot (Webhook)
typescript
// External service sends notification to Chat
async function sendNotification(webhookUrl: string, message: string) {
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
cardsV2: [{
cardId: `notif-${Date.now()}`,
card: {
header: { title: "Alert" },
sections: [{
widgets: [{
textParagraph: { text: message }
}]
}]
}
}]
})
})
}
When to use: CI/CD alerts, monitoring notifications, event triggers
Pattern 2: Interactive Form
typescript
// Show form to collect data
function showForm() {
return {
cardsV2: [{
cardId: "form-card",
card: {
header: { title: "Enter Details" },
sections: [{
widgets: [
{
textInput: {
name: "email",
label: "Email",
type: "SINGLE_LINE",
hintText: "user@example.com"
}
},
{
selectionInput: {
name: "priority",
label: "Priority",
type: "DROPDOWN",
items: [
{ text: "Low", value: "low" },
{ text: "High", value: "high" }
]
}
},
{
buttonList: {
buttons: [{
text: "Submit",
onClick: {
action: {
function: "submitForm",
parameters: [{
key: "formId",
value: "contact-form"
}]
}
}
}]
}
}
]
}]
}
}]
}
}
When to use: Data collection, approval workflows, ticket creation
Pattern 3: Dialog (Modal)
typescript
// Open modal dialog
function openDialog() {
return {
actionResponse: {
type: "DIALOG",
dialogAction: {
dialog: {
body: {
sections: [{
header: "Confirm Action",
widgets: [{
textParagraph: { text: "Are you sure?" }
}, {
buttonList: {
buttons: [
{
text: "Confirm",
onClick: {
action: { function: "confirm" }
}
},
{
text: "Cancel",
onClick: {
action: { function: "cancel" }
}
}
]
}
}]
}]
}
}
}
}
}
}
When to use: Confirmations, multi-step workflows, focused data entry
Using Bundled Resources
Scripts (scripts/)
No executable scripts for this skill.
Templates (templates/)
Required for all projects:
templates/webhook-handler.ts
- Basic webhook receiver
- - Cloudflare Workers config
Optional based on needs:
templates/interactive-bot.ts
- HTTP endpoint with event handling
templates/card-builder-examples.ts
- Common card patterns
templates/form-validation.ts
- Input validation with error responses
templates/bearer-token-verify.ts
- Token verification utility
When to load these: Claude should reference templates when user asks to:
- Set up Google Chat bot
- Create interactive cards
- Add form validation
- Verify bearer tokens
- Handle button clicks
References (references/)
references/google-chat-docs.md
- Key documentation links
references/cards-v2-schema.md
- Complete card structure reference
references/common-errors.md
- Error troubleshooting guide
When Claude should load these: Troubleshooting errors, designing cards, understanding API
Advanced Topics
Slash Commands
Register slash commands for quick actions:
typescript
// User types: /create-ticket Bug in login
if (event.message?.slashCommand?.commandName === 'create-ticket') {
const text = event.message.argumentText
return Response.json({
text: `Creating ticket: ${text}`,
cardsV2: [/* ticket confirmation card */]
})
}
Use cases: Quick actions, shortcuts, power user features
Thread Replies
Reply in existing thread:
typescript
return Response.json({
text: "Reply in thread",
thread: {
name: event.message.thread.name // Use existing thread
}
})
Use cases: Conversations, follow-ups, grouped discussions
Spaces API
Programmatically manage Google Chat spaces (rooms). Requires
Chat Admin or App permissions.
Available Methods
| Method | Description | Scope Required |
|---|
| Create new space | |
| Delete a space | |
| Get space details | |
| List spaces bot is in | |
| Update space settings | |
| Search spaces by criteria | |
| Create space and add members | |
| Find DM with specific user | |
Create a Space
typescript
async function createSpace(accessToken: string) {
const response = await fetch('https://chat.googleapis.com/v1/spaces', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
spaceType: 'SPACE', // or 'GROUP_CHAT', 'DIRECT_MESSAGE'
displayName: 'Project Team',
singleUserBotDm: false,
spaceDetails: {
description: 'Team collaboration space',
guidelines: 'Be respectful and on-topic'
}
})
})
return response.json()
}
List Spaces (Bot's Accessible Spaces)
typescript
async function listSpaces(accessToken: string) {
const response = await fetch(
'https://chat.googleapis.com/v1/spaces?pageSize=100',
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
)
const data = await response.json()
// Returns: { spaces: [...], nextPageToken: '...' }
return data.spaces
}
Search Spaces
typescript
async function searchSpaces(accessToken: string, query: string) {
const params = new URLSearchParams({
query: query, // e.g., 'displayName:Project'
pageSize: '50'
})
const response = await fetch(
`https://chat.googleapis.com/v1/spaces:search?${params}`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
)
return response.json()
}
Search Query Syntax:
- - Name contains "Project"
- - Only spaces (not DMs)
- - Created after date
- Combine with / operators
Members API
Manage space membership programmatically. Requires
User or App authorization.
Available Methods
| Method | Description | Scope Required |
|---|
| Add member to space | |
| Remove member | |
| Get member details | chat.memberships.readonly
|
| List all members | chat.memberships.readonly
|
| Update member role | |
Add Member to Space
typescript
async function addMember(accessToken: string, spaceName: string, userEmail: string) {
const response = await fetch(
`https://chat.googleapis.com/v1/${spaceName}/members`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
member: {
name: `users/${userEmail}`,
type: 'HUMAN' // or 'BOT'
},
role: 'ROLE_MEMBER' // or 'ROLE_MANAGER'
})
}
)
return response.json()
}
List Space Members
typescript
async function listMembers(accessToken: string, spaceName: string) {
const response = await fetch(
`https://chat.googleapis.com/v1/${spaceName}/members?pageSize=100`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
)
return response.json()
// Returns: { memberships: [...], nextPageToken: '...' }
}
Update Member Role
typescript
async function updateMemberRole(
accessToken: string,
memberName: string, // e.g., 'spaces/ABC/members/DEF'
newRole: 'ROLE_MEMBER' | 'ROLE_MANAGER'
) {
const response = await fetch(
`https://chat.googleapis.com/v1/${memberName}?updateMask=role`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ role: newRole })
}
)
return response.json()
}
Member Roles:
- - Standard member (read/write messages)
- - Can manage space settings and members
Reactions API
Add emoji reactions to messages. Added in 2025, supports custom workspace emojis.
Available Methods
| Method | Description |
|---|
spaces.messages.reactions.create
| Add reaction to message |
spaces.messages.reactions.delete
| Remove reaction |
spaces.messages.reactions.list
| List reactions on message |
Add Reaction
typescript
async function addReaction(
accessToken: string,
messageName: string, // e.g., 'spaces/ABC/messages/XYZ'
emoji: string
) {
const response = await fetch(
`https://chat.googleapis.com/v1/${messageName}/reactions`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
emoji: {
unicode: emoji // e.g., '👍' or custom emoji code
}
})
}
)
return response.json()
}
List Reactions
typescript
async function listReactions(accessToken: string, messageName: string) {
const response = await fetch(
`https://chat.googleapis.com/v1/${messageName}/reactions?pageSize=100`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
)
return response.json()
// Returns: { reactions: [...], nextPageToken: '...' }
}
Custom Emoji: Workspace administrators can upload custom emoji. Use the emoji's
instead of
.
Rate Limits
Google Chat API enforces strict quotas to prevent abuse. Understanding these limits is critical for production apps.
Per-Project Quotas (Per Minute)
| Operation | Limit | Notes |
|---|
| Read operations | 3,000/min | spaces.get, members.list, messages.list |
| Membership writes | 300/min | members.create, members.delete |
| Space writes | 60/min | spaces.create, spaces.patch |
| Message operations | 600/min | messages.create, reactions.create |
| Reactions | 600/min | Shared with message operations |
Per-Space Quotas (Per Second)
| Operation | Limit |
|---|
| Read operations | 15/sec |
| Write operations | 1/sec |
Per-User Quotas
User-authenticated requests are also throttled per user:
- 60 requests/minute per user for most operations
- 10 requests/minute for space creation
Handling Rate Limit Errors
typescript
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error: any) {
if (error.status === 429) {
// Rate limited - wait with exponential backoff
const waitMs = Math.pow(2, i) * 1000 + Math.random() * 1000
await new Promise(r => setTimeout(r, waitMs))
continue
}
throw error
}
}
throw new Error('Max retries exceeded')
}
// Usage
const spaces = await withRetry(() => listSpaces(accessToken))
Best Practices:
- Cache read operations where possible
- Batch membership operations
- Use pagination efficiently (request larger pages, fewer requests)
- Implement exponential backoff for 429 errors
- Monitor quota usage in Google Cloud Console
Dependencies
Required:
- Cloudflare Workers account (free tier works)
- Google Cloud Project with Chat API enabled
- Public HTTPS endpoint (Workers provides this)
Optional:
- - Type-safe card builder (unofficial)
- Web Crypto API (built into Cloudflare Workers)
Official Documentation
Package Versions (Verified 2026-01-09)
json
{
"dependencies": {
"google-chat-cards": "^1.0.3"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260109.0",
"wrangler": "^4.58.0"
}
}
Note: No official Google Chat npm package - use fetch API directly.
Production Example
This skill is based on real-world implementations:
- Community Examples: translatebot (Worker + Chat + Translate API)
- Official Samples: Multiple working examples in Google's documentation
Token Savings: ~65-70% (8k → 2.5k tokens)
Errors Prevented: 6/6 documented issues
Validation: ✅ Webhook handlers, ✅ Card builders, ✅ Token verification, ✅ Form validation, ✅ Rate limit handling
Troubleshooting
Problem: "Unauthorized" (401) error
Solution: Implement bearer token verification (see
templates/bearer-token-verify.ts
)
Problem: Cards don't render / "Invalid JSON payload"
Solution: Validate card JSON against Cards v2 schema, ensure exact field names
Problem: Widgets beyond first 100 don't show
Solution: Split into multiple cards or use pagination
Problem: Form validation errors not showing to user
Solution: Return correct error format with
actionResponse.dialogAction.actionStatus
Problem: "Unable to connect to bot"
Solution: Ensure URL is publicly accessible, responds within timeout, returns valid JSON
Complete Setup Checklist
Use this checklist to verify your setup:
Questions? Issues?
- Check
references/common-errors.md
for troubleshooting
- Verify card JSON structure matches Cards v2 schema
- Check official docs: https://developers.google.com/workspace/chat
- Ensure bearer token verification is implemented for HTTP endpoints