Repository Public
1# Webhooks23> [!NOTE]4> **Note** : This version of the page covers the new [Interactions API](https://ai.google.dev/gemini-api/docs/interactions/interactions-overview), which is currently in Beta. 5> For stable production deployments, we recommend you continue to use the `generateContent` API. You can use the toggle on this page to switch between the versions.67Webhooks allow the Gemini API to push real-time notifications to your server8when asynchronous or Long-Running Operations (LROs) complete. This replaces the9need to poll the API for status updates, reducing latency and overhead.1011Webhooks are available for operations like [Batch](https://ai.google.dev/gemini-api/docs/batch-api) jobs,12[Interactions](https://ai.google.dev/gemini-api/docs/interactions) and [video generation](https://ai.google.dev/gemini-api/docs/video).1314## How it works1516Instead of polling `GET /operations` repeatedly to check if a job is finished,17you can configure Gemini API Webhooks to send an HTTP POST request to your18listener URL immediately upon an event trigger.1920The Gemini API supports two ways to configure webhooks:2122- [**Static webhooks**](https://ai.google.dev/gemini-api/docs/interactions/webhooks#static-webhooks): Project-level endpoints configured with the Gemini [WebhookService API](https://ai.google.dev/api). Good for global integrations (e.g., notifying Slack, syncing a database, etc.).23- [**Dynamic webhooks**](https://ai.google.dev/gemini-api/docs/interactions/webhooks#dynamic-webhooks): Request-level overrides passing a webhook URL in the configuration payload of a specific jobs call. Ideal for routing specific jobs to dedicated endpoints.2425## Static webhooks2627Static webhooks are registered for a whole [project](https://ai.google.dev/gemini-api/docs/api-key#google-cloud-projects) and trigger for any matching28event.2930### Create a webhook3132You can create endpoints using the SDK or REST API.3334**IMPORTANT** : When creating a webhook, the API returns a **signing secret**35**only once** . You must store this securely (e.g. in your environment variables)36to verify signatures later. If you lose the signing secret, you'll have to37[rotate](https://ai.google.dev/gemini-api/docs/interactions/webhooks#rotate-signing-secret) it.3839### Python4041 from google import genai4243 client = genai.Client()4445 webhook = client.webhooks.create(46 name="MyBatchWebhook",47 subscribed_events=["batch.succeeded", "batch.failed"],48 uri="https://my-api.com/gemini-callback",49 )5051 # Store webhook.new_signing_secret securely52 webhook_secret = webhook.new_signing_secret53 print(f"Created webhook: {webhook.name}, {webhook.id}")5455### JavaScript5657 import { GoogleGenAI } from "@google/genai";5859 const client = new GoogleGenAI();6061 async function createWebhook() {62 const webhook = await client.webhooks.create({63 name: "MyBatchWebhook",64 subscribed_events: ["batch.succeeded", "batch.failed"],65 uri: "https://my-api.com/gemini-callback",66 });6768 // Store webhook.signingSecret securely69 const webhookSecret = webhook.new_signing_secret;70 console.log(`Created webhook: ${webhook.name}, ${webhook.id}`);71 }7273 createWebhook();7475### REST7677 curl -X POST \78 "https://generativelanguage.googleapis.com/v1/webhooks" \79 -H "Content-Type: application/json" \80 -H "x-goog-api-key: $GEMINI_API_KEY" \81 -d '{82 "name": "MyBatchWebhook",83 "uri": "https://my-api.com/gemini-callback",84 "subscribed_events": ["batch.succeeded", "batch.failed"]85 }'8687For details on setting up your server to receive data, see the88[Handle webhook requests](https://ai.google.dev/gemini-api/docs/interactions/webhooks#handle-webhook-requests) section.8990### Get a webhook9192Retrieve details about a specific webhook by its resource name.9394### Python9596 from google import genai9798 client = genai.Client()99100 webhook = client.webhooks.get(id="<your_webhook_id>")101102 print(f"Webhook: {webhook.name}")103 print(f"URI: {webhook.uri}")104 print(f"Events: {webhook.subscribed_events}")105106### JavaScript107108 import { GoogleGenAI } from "@google/genai";109110 const client = new GoogleGenAI(); // Assumes process.env.GEMINI_API_KEY is set111112 async function getWebhook() {113 const webhook = await client.webhooks.get("<your_webhook_id>");114115 console.log(`Webhook: ${webhook.name}`);116 console.log(`URI: ${webhook.uri}`);117 console.log(`Events: ${webhook.subscribed_events}`);118 }119120 getWebhook();121122### REST123124 curl -X GET \125 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \126 -H "x-goog-api-key: $GEMINI_API_KEY"127128### List webhooks129130List all configured webhooks for the current project, with optional pagination.131132### Python133134 from google import genai135136 client = genai.Client()137138 webhooks = client.webhooks.list()139140 for wh in webhooks:141 print(f"{wh.id}: {wh.name} -> {wh.uri}")142143### JavaScript144145 import { GoogleGenAI } from "@google/genai";146147 const client = new GoogleGenAI();148149 async function listWebhooks() {150 const webhooks = await client.webhooks.list();151152 for (const wh of webhooks) {153 console.log(`${wh.id}: ${wh.name} -> ${wh.uri}`);154 }155 }156157 listWebhooks();158159### REST160161 curl -X GET \162 "https://generativelanguage.googleapis.com/v1/webhooks" \163 -H "x-goog-api-key: $GEMINI_API_KEY"164165### Update a webhook166167Update an existing webhook's properties such as the display name, target URI, or168subscribed events.169170### Python171172 from google import genai173174 client = genai.Client()175176 updated_webhook = client.webhooks.update(177 id="<your_webhook_id>",178 subscribed_events=["batch.succeeded", "batch.failed", "batch.cancelled"],179 )180181 print(f"Updated webhook: {updated_webhook.name}")182183### JavaScript184185 import { GoogleGenAI } from "@google/genai";186187 const client = new GoogleGenAI();188189 async function updateWebhook() {190 const updatedWebhook = await client.webhooks.update(191 "<your_webhook_id>",192 {193 subscribed_events: ["batch.succeeded", "batch.failed", "batch.cancelled"],194 }195 );196197 console.log(`Updated webhook: ${updatedWebhook.name}`);198 }199200 updateWebhook();201202### REST203204 curl -X PATCH \205 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \206 -H "Content-Type: application/json" \207 -H "x-goog-api-key: $GEMINI_API_KEY" \208 -d '{209 "subscribed_events": ["batch.succeeded", "batch.failed", "batch.cancelled"]210 }'211212### Delete a webhook213214Remove a webhook endpoint from the project. This stops future event deliveries215to that endpoint.216217### Python218219 from google import genai220221 client = genai.Client()222223 client.webhooks.delete(id="<your_webhook_id>")224225 print("Webhook deleted.")226227### JavaScript228229 import { GoogleGenAI } from "@google/genai";230231 const client = new GoogleGenAI();232233 async function deleteWebhook() {234 await client.webhooks.delete("<your_webhook_id>");235236 console.log("Webhook deleted.");237 }238239 deleteWebhook();240241### REST242243 curl -X DELETE \244 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \245 -H "x-goog-api-key: $GEMINI_API_KEY"246247### Rotate a signing secret248249Rotate the signing secret for a webhook. You can configure whether previously250active secrets are revoked immediately or after a 24-hour grace period.251252**IMPORTANT** : The new signing secret is returned **only once** at rotation253time. Store it securely before updating your verification logic.254255### Python256257 from google import genai258 from google.genai import types259260 client = genai.Client()261262 response = client.webhooks.rotate_signing_secret(263 id="<your_webhook_id>",264 revocation_behavior="REVOKE_PREVIOUS_SECRETS_AFTER_H24",265 )266267 # Store response.secret securely, then update your server's verification config268 print("New signing secret generated. Update your server configuration.")269270### JavaScript271272 import { GoogleGenAI } from "@google/genai";273274 const client = new GoogleGenAI();275276 async function rotateSigningSecret() {277 const response = await client.webhooks.rotateSigningSecret(278 "<your_webhook_id>",279 {280 revocation_behavior: "REVOKE_PREVIOUS_SECRETS_AFTER_H24",281 }282 );283284 // Store response.secret securely, then update your server's verification config285 console.log("New signing secret generated. Update your server configuration.");286 }287288 rotateSigningSecret();289290### REST291292 curl -X POST \293 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>/rotate_secret" \294 -H "Content-Type: application/json" \295 -H "x-goog-api-key: $GEMINI_API_KEY" \296 -d '{297 "revocation_behavior": "REVOKE_PREVIOUS_SECRETS_AFTER_H24"298 }'299300### Handle webhook requests on a server301302When an event happens that you're subscribed to, your webhook URL will receive303an HTTP POST request. Your endpoint must respond with a 2xx status304code within a few seconds to avoid a retry. To ensure delivery, the Gemini API305automatically retries failed requests for 24 hours using exponential backoff.306307Gemini strictly follows the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification for308security headers. Verify the payload on your server using the signed header309signatures and your stored static signing secret. See the [Webhook envelope](https://ai.google.dev/gemini-api/docs/interactions/webhooks#webhook-envelope) section for the payload information.310311Here is an example using Flask for the HTTP listener:312313### Python314315 # pip install flask standardwebhooks316 import os317 from flask import Flask, request, jsonify318 # Standard verification wrapper for Standard Webhook Headers319 from standardwebhooks.webhooks import Webhook, WebhookVerificationError320321 app = Flask(__name__)322323 SIGNING_SECRET = os.environ.get('WEBHOOK_SIGNING_SECRET')324325 @app.route('/gemini-callback', methods=['POST'])326 def gemini_callback():327 payload = request.get_data(as_text=True)328 headers = request.headers329330 try:331 wh = Webhook(SIGNING_SECRET)332 event = wh.verify(payload, headers)333 except WebhookVerificationError as e:334 return jsonify({"error": "Signature invalid"}), 400335336 # Process thin payload contents337 if event.get("type") == "batch.succeeded":338 print(f"Batch completed! ID: {event['data']['id']}")339 if event["data"].get("output_file_uri"):340 # For batch jobs with input file341 print(f"Batch file: {event['data']['output_file_uri']}")342 elif event.get("type") == "interaction.completed":343 print(f"Interaction completed! ID: {event['data']['id']}")344 elif event.get("type") == "video.generated":345 print(f"Video generated! URI: {event['data']['output_file_uri']}")346347 return jsonify({"status": "received"}), 200348349 if __name__ == "__main__":350 app.run(port=8000)351352### JavaScript353354 // npm install standardwebhooks355 import { Webhook } from "standardwebhooks";356 import express from "express";357358 const app = express();359 const client = new GoogleGenAI({ webhookSecret: process.env.WEBHOOK_SIGNING_SECRET });360361 // Don't use express.json() because signature verification needs the raw text body362 app.use(express.text({ type: "application/json" }));363364 app.post("/gemini-callback", async (req, res) => {365 const payload = await req.text();366 const headers: Record<string, string> = {};367 req.headers.forEach((value, key) => {368 headers[key] = value;369 });370371 try {372 const wh = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);373 const event = wh.verify(payload, headers) as Record<string, any>;374 console.log(`Event type: ${event.type}, data: ${JSON.stringify(event.data)}`);375376 // Process thin payload contents377 if (event.type === "batch.succeeded") {378 console.log(`Batch completed! ID: ${event.data.id}`);379 if (event.data.output_file_uri) {380 // For batch jobs with input file381 console.log(`Batch file: ${event.data.output_file_uri}`);382 }383 } else if (event.type === "interaction.completed") {384 console.log(`Interaction completed! ID: ${event.data.id}`);385 } else if (event.type === "video.generated") {386 console.log(`Video generated! URI: ${event.data.output_file_uri}`);387 }388389 res.status(200).json({ status: "received" });390 } catch (e) {391 console.error("Webhook verification failed:", e);392 res.status(400).send("Invalid signature");393 }394 });395396 app.listen(8000, () => {397 console.log("Webhook server is running on port 8000");398 });399400## Dynamic webhooks401402Dynamic webhooks allow you to bind a webhook endpoint to a **specific request403configuration**, ideal for agent-orchestration queues. Dynamic webhooks leverage404asymmetric public-key JWKS signatures instead of symmetric secrets.405406### Submit a dynamic request407408Add a `webhook_config` when triggering an asynchronous job (e.g., creating a409Batch).410411### Python412413 # This will only work for SDK newer than 2.0.0414 from google import genai415416 client = genai.Client()417418 response = client.interactions.create(419 model='gemini-3.5-flash',420 input='Tell me a short joke about programming.',421 background=True, # Required when webhook_config is specified422 webhook_config={423 'uris': ["https://my-api.com/gemini-webhook-dynamic"],424 'user_metadata': {"job_group": "nightly-eval", "priority": "high"}425 }426 )427428 print(f"Interaction created! ID: {response.id}")429 print(f"Status: {response.status}")430431### JavaScript432433 // This will only work for SDK newer than 2.0.0434 import { GoogleGenAI } from "@google/genai";435436 const client = new GoogleGenAI();437438 async function createInteractionWithWebhook() {439 const response = await client.interactions.create({440 model: "gemini-3.5-flash",441 input: "Tell me a short joke about programming.",442 background: true, // Required when webhook_config is specified443 webhook_config: {444 uris: ["https://my-api.com/gemini-webhook-dynamic"],445 user_metadata: { job_group: "nightly-eval", priority: "high" },446 },447 });448449 console.log(`Interaction created! ID: ${response.id}`);450 console.log(`Status: ${response.status}`);451 }452453 createInteractionWithWebhook();454455### REST456457 # Specifies the API revision to avoid breaking changes when they become default458 curl -X POST \459 "https://generativelanguage.googleapis.com/v1beta/interactions" \460 -H "Content-Type: application/json" \461 -H "x-goog-api-key: $GEMINI_API_KEY" \462 -H "Api-Revision: 2026-05-20" \463 -d '{464 "model": "gemini-3.5-flash",465 "input": "Tell me a short joke about programming.",466 "background": true,467 "webhook_config": {468 "uris": ["https://my-api.com/gemini-webhook-dynamic"],469 "user_metadata": {"job_group": "nightly-eval", "priority": "high"}470 }471 }'472473### Verify dynamic signatures (JWKS)474475Dynamic webhook requests emit a JSON Web Token (JWT) signature. Your listener476must extract the signature and verify it using [Google's public certificate477endpoints](https://www.googleapis.com/oauth2/v3/certs).478479### Python480481 import jwt482 import requests483 from flask import Flask, request, jsonify484485 app = Flask(__name__)486487 # Google public cert list endpoint488 JWKS_URI = "https://generativelanguage.googleapis.com/.well-known/jwks.json"489490 def load_google_public_key(kid):491 response = requests.get(JWKS_URI).json()492 for key_item in response.get('keys', []):493 if key_item.get('kid') == kid:494 # Convert JWK to Cert wrapper495 return jwt.algorithms.RSAAlgorithm.from_jwk(key_item)496 return None497498 @app.route('/gemini-webhook-dynamic', methods=['POST'])499 def dynamic_handler():500 payload = request.get_data(as_text=True)501 headers = request.headers502503 token = headers.get('Webhook-Signature')504 if not token:505 return jsonify({"error": "No signature header"}), 400506507 try:508 # Extract kid from JWT header509 unverified_headers = jwt.get_unverified_header(token)510 pub_key = load_google_public_key(unverified_headers.get('kid'))511512 if not pub_key:513 return jsonify({"error": "Key cert not found"}), 400514515 # Verify Signature against expected audience (e.g., your project client ID)516 event = jwt.decode(517 token,518 pub_key,519 algorithms=["RS256"],520 audience="your-configured-audience"521 )522 except Exception as e:523 return jsonify({"error": "Invalid Dynamic signature", "details": str(e)}), 400524525 print("Verified Dynamic payload success.")526 return jsonify({"status": "received"}), 200527528### JavaScript529530 import { GoogleGenAI } from "@google/genai";531 import express from "express";532 import jwt from "jsonwebtoken";533 import jwksClient from "jwks-rsa";534535 const app = express();536 app.use(express.text({ type: 'application/json' }));537538 const client = jwksClient({539 jwksUri: "https://generativelanguage.googleapis.com/.well-known/jwks.json"540 });541542 function getKey(header, callback) {543 client.getSigningKey(header.kid, (err, key) => {544 const signingKey = key.getPublicKey();545 callback(null, signingKey);546 });547 }548549 app.post('/gemini-webhook-dynamic', (req, res) => {550 const token = req.headers['webhook-signature'];551552 if (!token) {553 return res.status(400).json({ error: "No signature header" });554 }555556 jwt.verify(557 token,558 getKey,559 {560 algorithms: ["RS256"],561 audience: "your-configured-audience"562 },563 (err, decoded) => {564 if (err) {565 return res.status(400).json({ error: "Invalid Dynamic signature", details: err.message });566 }567568 console.log("Verified Dynamic payload success.");569 res.status(200).json({ status: "received" });570 }571 );572 });573574## Webhook envelope575576To avoid bandwidth congestion, Gemini webhooks use a **thin payload** model to577deliver data.578deliveries send a snapshot containing status details and pointers to results,579rather than the raw output file itself.580581Here is an example payload format:582583 {584 "type": "batch.succeeded",585 "version": "v1",586 "timestamp": "2026-01-22T12:00:00Z",587 "data": {588 "id": "batch_123456",589 "output_file_uri": "gs://my-bucket/results.jsonl"590 }591 }592593## Event catalog reference594595The following events are triggered for supporting jobs:596597| Event type | Trigger | Payload item (`data`) |598|---|---|---|599| `batch.succeeded` | Processing finished successfully. | `id`, `output_file_uri` |600| `batch.cancelled` | User cancelled request | `id` |601| `batch.expired` | Batch has not been processed (finished) in 24h timeframe | `id` |602| `batch.failed` | Batch job failed (system or validation error). | `id`, `error_code`, `error_message` |603| `interaction.requires_action` | Function call, user needs to do something | `id` |604| `interaction.completed` | LRO in interactions API succeeded | `id` |605| `interaction.failed` | LRO in interactions API failed (system or validation error). | `id`, `error_code`, `error_message` |606| `interaction.cancelled` | LRO in interactions API cancelled | `id` |607| `video.generated` | Video generation LRO completed. | `id`, `output_file_uri`, `file_name` |608609## Best practices610611To ensure reliable, scalable operation:612613- **Strict replay protection check** : All requests carry a `webhook-timestamp` header. Always validate this timestamp on your server configuration layer to reject payloads older than **5 minutes** (to mitigate replay attacks).614- **Process asynchronously** : Respond with `2xx OK` immediately upon valid signature detection, and queue parsing operations internally. Prolonged listener hold times will trigger a delivery retry cycle.615- **Deduplication handling** : Standard webhooks deliver "At-least-once". Use the consistent `webhook-id` header to handle potential duplicates in higher congestion flows.616617## What's next?618619- [Batch API](https://ai.google.dev/gemini-api/docs/batch): Utilize webhooks to automate high volume endpoints.