gemini-webhooks
Receive and verify Google Gemini API webhooks. Use when setting up Gemini webhook handlers for batch jobs, video generation, or Interactions API function-calling LROs, debugging signature verification, or handling events like batch.succeeded, batch.failed, video.generated, or interaction.completed.
What this skill does
# Gemini Webhooks
## When to Use This Skill
- Setting up Google Gemini API webhook handlers
- Debugging Gemini webhook signature verification failures
- Handling `batch.succeeded` / `batch.failed` notifications for the Batch API
- Handling `video.generated` notifications for the Veo/video generation API
- Handling `interaction.completed` / `interaction.requires_action` events for the Interactions API
- Replacing polling for long-running Gemini operations (LROs)
- Verifying Standard Webhooks-format signatures from Google `generativelanguage.googleapis.com`
## Essential Code (USE THIS)
Gemini webhooks follow the [Standard Webhooks](https://www.standardwebhooks.com/) specification.
Each delivery includes three headers:
- `webhook-id` — unique message id (use for idempotency)
- `webhook-timestamp` — Unix seconds (reject if > 5 minutes old)
- `webhook-signature` — one or more space-separated `v1,<base64-hmac-sha256>` entries over `webhook-id.webhook-timestamp.body` (multiple entries appear during secret rotation)
The signing secret is returned once when the webhook is created via the WebhookService API
and is base64-encoded, prefixed with `whsec_`.
### Express Webhook Handler
```javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
function verifyGeminiSignature(payload, webhookId, webhookTimestamp, webhookSignature, secret) {
if (!webhookId || !webhookTimestamp || !webhookSignature || !webhookSignature.includes(',')) {
return false;
}
// Reject payloads older than 5 minutes to prevent replay attacks
const currentTime = Math.floor(Date.now() / 1000);
const timestampDiff = currentTime - parseInt(webhookTimestamp);
if (timestampDiff > 300 || timestampDiff < -300) {
return false;
}
// Signed content: webhook_id.webhook_timestamp.raw_body
const payloadStr = payload instanceof Buffer ? payload.toString('utf8') : payload;
const signedContent = `${webhookId}.${webhookTimestamp}.${payloadStr}`;
// Strip whsec_ prefix and base64-decode the secret
const secretKey = secret.startsWith('whsec_') ? secret.slice(6) : secret;
const secretBytes = Buffer.from(secretKey, 'base64');
const expectedSignature = crypto
.createHmac('sha256', secretBytes)
.update(signedContent, 'utf8')
.digest('base64');
const expectedBuf = Buffer.from(expectedSignature);
// Standard Webhooks allows space-separated entries during secret rotation:
// `v1,<sig1> v1,<sig2>`. Accept the message if any v1 entry matches.
for (const part of webhookSignature.split(' ')) {
const commaIdx = part.indexOf(',');
if (commaIdx === -1) continue;
const version = part.slice(0, commaIdx);
const signature = part.slice(commaIdx + 1);
if (version !== 'v1') continue;
const sigBuf = Buffer.from(signature);
if (sigBuf.length !== expectedBuf.length) continue;
try {
if (crypto.timingSafeEqual(sigBuf, expectedBuf)) return true;
} catch {
// length mismatch — try the next entry
}
}
return false;
}
// CRITICAL: use express.raw() — signature is computed over the raw body
app.post('/webhooks/gemini',
express.raw({ type: 'application/json' }),
(req, res) => {
const webhookId = req.headers['webhook-id'];
const webhookTimestamp = req.headers['webhook-timestamp'];
const webhookSignature = req.headers['webhook-signature'];
if (!verifyGeminiSignature(
req.body,
webhookId,
webhookTimestamp,
webhookSignature,
process.env.GEMINI_WEBHOOK_SECRET
)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'batch.succeeded':
console.log(`Batch succeeded: ${event.data.id}`);
break;
case 'batch.failed':
console.log(`Batch failed: ${event.data.id}`);
break;
case 'batch.cancelled':
console.log(`Batch cancelled: ${event.data.id}`);
break;
case 'batch.expired':
console.log(`Batch expired: ${event.data.id}`);
break;
case 'video.generated':
console.log(`Video generated: ${event.data.id}`);
break;
case 'interaction.completed':
console.log(`Interaction completed: ${event.data.id}`);
break;
case 'interaction.requires_action':
console.log(`Interaction requires action: ${event.data.id}`);
break;
case 'interaction.failed':
console.log(`Interaction failed: ${event.data.id}`);
break;
case 'interaction.cancelled':
console.log(`Interaction cancelled: ${event.data.id}`);
break;
default:
console.log(`Unhandled event: ${event.type}`);
}
res.json({ received: true });
}
);
```
### Python (FastAPI) Webhook Handler
```python
import os
import hmac
import hashlib
import base64
import time
from fastapi import FastAPI, Request, HTTPException, Header
app = FastAPI()
def verify_gemini_signature(
payload: bytes,
webhook_id: str,
webhook_timestamp: str,
webhook_signature: str,
secret: str
) -> bool:
if not webhook_id or not webhook_timestamp or not webhook_signature or ',' not in webhook_signature:
return False
current_time = int(time.time())
try:
timestamp_diff = current_time - int(webhook_timestamp)
except ValueError:
return False
if timestamp_diff > 300 or timestamp_diff < -300:
return False
signed_content = f"{webhook_id}.{webhook_timestamp}.{payload.decode('utf-8')}"
secret_key = secret[6:] if secret.startswith('whsec_') else secret
secret_bytes = base64.b64decode(secret_key)
expected_signature = base64.b64encode(
hmac.new(secret_bytes, signed_content.encode('utf-8'), hashlib.sha256).digest()
).decode('utf-8')
# Standard Webhooks allows space-separated entries during secret rotation:
# `v1,<sig1> v1,<sig2>`. Accept the message if any v1 entry matches.
for part in webhook_signature.split(' '):
if ',' not in part:
continue
version, _, signature = part.partition(',')
if version != 'v1':
continue
if hmac.compare_digest(signature, expected_signature):
return True
return False
@app.post("/webhooks/gemini")
async def gemini_webhook(
request: Request,
webhook_id: str = Header(None, alias="webhook-id"),
webhook_timestamp: str = Header(None, alias="webhook-timestamp"),
webhook_signature: str = Header(None, alias="webhook-signature"),
):
payload = await request.body()
if not verify_gemini_signature(
payload,
webhook_id,
webhook_timestamp,
webhook_signature,
os.environ.get("GEMINI_WEBHOOK_SECRET", "")
):
raise HTTPException(status_code=400, detail="Invalid signature")
event = await request.json()
# Handle event...
return {"received": True}
```
> **For complete working examples with tests**, see:
> - [examples/express/](examples/express/) - Full Express implementation
> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation
> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation
## Common Event Types
| Event | Description |
|-------|-------------|
| `batch.succeeded` | Batch API job processing finished successfully |
| `batch.failed` | Batch API job hit a system or validation error |
| `batch.cancelled` | Batch API job was cancelled by the user |
| `batch.expired` | Batch API job did not complete within 24 hours |
| `video.generated` | Video generation (Veo) completed |
| `interaction.completed` | Long-running Interactions API call succeeded |
| `interaction.requires_action` | Interactions API call needs a function-call result |
| `interaction.failed` | Interactions API call failed |
| `interaction.cancelled` | Interactions API call was cancelled |
> **For the full event reference**, see [Gemini API webhooks](https://ai.Related in Image & Video
watch
IncludedWatch a video (URL or local path). Downloads with yt-dlp, extracts auto-scaled frames with ffmpeg, pulls the transcript from captions (or Whisper API fallback), and hands the result to Claude so it can answer questions about what's in the video.
physical-ai-defect-image-generation
IncludedUse when the user wants to orchestrate defect image generation, run associated setup, or handle outputs on OSMO. The Day 0 path handles cold-start with USD-to-ROI, image-edit augmentation, and AnomalyGen to create initial PCBA datasets. The Day 1 path performs inference and labeling on real images. This skill helps with first-time asset setup, creation of finetuning checkpoints, and configuring deployment. Trigger keywords: defect image generation, dig workflow, dig pipeline, defect image detection workflow, aoi pipeline, aoi anomalygen, usd2roi anomalygen, day 0 pcba, day 1 pcba, day 1 real-photo alignment, day 1 manual roi, metal surface anomaly, glass defect, anomalygen finetune, setup_pcb, setup_metal, setup_glass, setup_pretrained, dig setup, dig datasets, dig pretrained checkpoint, dig image-edit endpoint.
accelint-react-best-practices
IncludedReact performance optimization and best practices. ALWAYS use this skill when working with any React code - writing components, hooks, JSX; refactoring; optimizing re-renders, memoization, state management; reviewing for performance; fixing hydration mismatches; debugging infinite re-renders, stale closures, input focus loss, animations restarting; preventing remounting; implementing transitions, lazy initialization, effect dependencies. Even simple React tasks benefit from these patterns. Covers React 19+ (useEffectEvent, Activity, ref props). Triggers - useEffect, useState, useMemo, useCallback, memo, inline components, nested components, components inside components, re-render, performance, hydration, SSR, Next.js, useDeferredValue, combined hooks.
elevenlabs-agents
IncludedBuild conversational AI voice agents with ElevenLabs Platform using React, JavaScript, React Native, or Swift SDKs. Configure agents, tools (client/server/MCP), RAG knowledge bases, multi-voice, and Scribe real-time STT. Use when: building voice chat interfaces, implementing AI phone agents with Twilio, configuring agent workflows or tools, adding RAG knowledge bases, testing with CLI "agents as code", or troubleshooting deprecated @11labs packages, Android audio cutoff, CSP violations, dynamic variables, or WebRTC config. Keywords: ElevenLabs Agents, ElevenLabs voice agents, AI voice agents, conversational AI, @elevenlabs/react, @elevenlabs/client, @elevenlabs/react-native, @elevenlabs/elevenlabs-js, @elevenlabs/agents-cli, elevenlabs SDK, voice AI, TTS, text-to-speech, ASR, speech recognition, turn-taking model, WebRTC voice, WebSocket voice, ElevenLabs conversation, agent system prompt, agent tools, agent knowledge base, RAG voice agents, multi-voice agents, pronunciation dictionary, voice speed control, elevenlabs scribe, @11labs deprecated, Android audio cutoff, CSP violation elevenlabs, dynamic variables elevenlabs, case-sensitive tool names, webhook authentication
humanizer
IncludedHumanize AI-generated text by detecting and removing patterns typical of LLM output. Rewrites text to sound natural, specific, and human. Uses 28 pattern detectors, 560+ AI vocabulary terms across 3 tiers, and statistical analysis (burstiness, type-token ratio, readability) for comprehensive detection. Use when asked to humanize text, de-AI writing, make content sound more natural/human, review writing for AI patterns, score text for AI detection, or improve AI-generated drafts. Covers content, language, style, communication, and filler categories.
generating-mermaid-diagrams
IncludedSalesforce architecture diagrams using Mermaid with ASCII fallback. Use this skill when generating text-based diagrams for Salesforce architecture, OAuth flows, ERDs, integration sequences, or Agentforce structure. TRIGGER when: user says "diagram", "visualize", "ERD", or asks for sequence diagrams, flowcharts, class diagrams, or architecture visualizations in Mermaid. DO NOT TRIGGER when: user wants PNG/SVG image output (use generating-visual-diagrams), or asks about non-Salesforce systems.