--- name: google-chat-api description: | Build Google Chat bots and webhooks with Cards v2, interactive forms, and Cloudflare Workers. Covers Spaces/Members/Reactions APIs, bearer token verification, and dialog patterns. Use when: creating Chat bots, workflow automation, interactive forms. Troubleshoot: bearer token 401, rate limit 429, card schema validation, webhook failures. user-invocable: true --- # 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 { 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 `cardsV2` 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 { // 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**: 1. Create Chat space 2. Configure incoming webhook in Space settings 3. 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**: 1. Create Google Cloud project 2. Enable Chat API 3. Configure Chat app with HTTP endpoint 4. Deploy Cloudflare Worker 5. 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**: - `textParagraph` - Text content - `buttonList` - Buttons (text or icon) - `textInput` - Text input field - `selectionInput` - Dropdowns, checkboxes, switches - `dateTimePicker` - Date/time selection - `divider` - Horizontal line - `image` - Images - `decoratedText` - 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 bold and italic text with color" } } // 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): - `**bold**` or `*italic*` - `` `code` `` for inline code - `- list item` or `1. ordered` for lists - ` ```code block``` ` for multi-line code - `~strikethrough~` **Supported HTML** (cards only): - `bold`, `italic`, `underline` - `colored` - `link` **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 { 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**: - `ADDED_TO_SPACE` - Bot added to space - `REMOVED_FROM_SPACE` - Bot removed - `MESSAGE` - User sent message - `CARD_CLICKED` - User clicked button/submitted form --- ## Critical Rules ### Always Do ✅ Return valid JSON with `cardsV2` array structure ✅ Set unique `cardId` 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 `google-chat-cards` 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:** - `nodejs_compat` - 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 - `templates/wrangler.jsonc` - 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](https://developers.google.com/workspace/chat/authenticate-authorize). ### Available Methods | Method | Description | Scope Required | |--------|-------------|----------------| | `spaces.create` | Create new space | `chat.spaces.create` | | `spaces.delete` | Delete a space | `chat.delete` | | `spaces.get` | Get space details | `chat.spaces.readonly` | | `spaces.list` | List spaces bot is in | `chat.spaces.readonly` | | `spaces.patch` | Update space settings | `chat.spaces` | | `spaces.search` | Search spaces by criteria | `chat.spaces.readonly` | | `spaces.setup` | Create space and add members | `chat.spaces.create` | | `spaces.findDirectMessage` | Find DM with specific user | `chat.spaces.readonly` | ### 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**: - `displayName:Project` - Name contains "Project" - `spaceType:SPACE` - Only spaces (not DMs) - `createTime>2025-01-01` - Created after date - Combine with `AND`/`OR` operators --- ## Members API Manage space membership programmatically. Requires [User or App authorization](https://developers.google.com/workspace/chat/authenticate-authorize). ### Available Methods | Method | Description | Scope Required | |--------|-------------|----------------| | `spaces.members.create` | Add member to space | `chat.memberships` | | `spaces.members.delete` | Remove member | `chat.memberships` | | `spaces.members.get` | Get member details | `chat.memberships.readonly` | | `spaces.members.list` | List all members | `chat.memberships.readonly` | | `spaces.members.patch` | Update member role | `chat.memberships` | ### 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**: - `ROLE_MEMBER` - Standard member (read/write messages) - `ROLE_MANAGER` - 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 `customEmoji.uid` instead of `unicode`. --- ## 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( fn: () => Promise, maxRetries = 3 ): Promise { 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**: - `google-chat-cards@1.0.3` - Type-safe card builder (unofficial) - Web Crypto API (built into Cloudflare Workers) --- ## Official Documentation - **Google Chat API**: https://developers.google.com/workspace/chat - **Cards v2 Reference**: https://developers.google.com/workspace/chat/api/reference/rest/v1/cards - **Webhooks Guide**: https://developers.google.com/workspace/chat/quickstart/webhooks - **Interactive Cards**: https://developers.google.com/workspace/chat/dialogs - **Cloudflare Workers**: https://developers.cloudflare.com/workers --- ## 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: - [ ] Google Cloud project created - [ ] Chat API enabled in project - [ ] Chat app configured with webhook/HTTP endpoint URL - [ ] Cloudflare Worker deployed and accessible - [ ] Bearer token verification implemented (if using HTTP endpoint) - [ ] Card JSON validated against schema - [ ] Widget count under 100 per card - [ ] Form validation returns correct error format - [ ] Tested in Chat space successfully - [ ] Error handling for all event types --- **Questions? Issues?** 1. Check `references/common-errors.md` for troubleshooting 2. Verify card JSON structure matches Cards v2 schema 3. Check official docs: https://developers.google.com/workspace/chat 4. Ensure bearer token verification is implemented for HTTP endpoints