Repository Public
1Webhooks allow the Gemini API to push real-time notifications to your server2when asynchronous or Long-Running Operations (LROs) complete. This replaces the3need to poll the API for status updates, reducing latency and overhead.45Webhooks are available for operations like [Batch](https://ai.google.dev/gemini-api/docs/batch-api) jobs,6[Interactions](https://ai.google.dev/gemini-api/docs/interactions-overview) and [video generation](https://ai.google.dev/gemini-api/docs/video).78## How it works910Instead of polling `GET /operations` repeatedly to check if a job is finished,11you can configure Gemini API Webhooks to send an HTTP POST request to your12listener URL immediately upon an event trigger.1314The Gemini API supports two ways to configure webhooks:1516- [**Static webhooks**](https://ai.google.dev/gemini-api/docs/generate-content/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.).17- [**Dynamic webhooks**](https://ai.google.dev/gemini-api/docs/generate-content/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.1819## Static webhooks2021Static webhooks are registered for a whole [project](https://ai.google.dev/gemini-api/docs/api-key#google-cloud-projects) and trigger for any matching22event.2324### Create a webhook2526You can create endpoints using the SDK or REST API.2728**IMPORTANT** : When creating a webhook, the API returns a **signing secret**29**only once** . You must store this securely (e.g. in your environment variables)30to verify signatures later. If you lose the signing secret, you'll have to31[rotate](https://ai.google.dev/gemini-api/docs/generate-content/webhooks#rotate-signing-secret) it.3233### Python3435 from google import genai3637 client = genai.Client()3839 webhook = client.webhooks.create(40 name="MyBatchWebhook",41 subscribed_events=["batch.succeeded", "batch.failed"],42 uri="https://my-api.com/gemini-callback",43 )4445 # Store webhook.new_signing_secret securely46 webhook_secret = webhook.new_signing_secret47 print(f"Created webhook: {webhook.name}, {webhook.id}")4849### JavaScript5051 import { GoogleGenAI } from "@google/genai";5253 const client = new GoogleGenAI();5455 async function createWebhook() {56 const webhook = await client.webhooks.create({57 name: "MyBatchWebhook",58 subscribed_events: ["batch.succeeded", "batch.failed"],59 uri: "https://my-api.com/gemini-callback",60 });6162 // Store webhook.signingSecret securely63 const webhookSecret = webhook.new_signing_secret;64 console.log(`Created webhook: ${webhook.name}, ${webhook.id}`);65 }6667 createWebhook();6869### REST7071 curl -X POST \72 "https://generativelanguage.googleapis.com/v1/webhooks" \73 -H "Content-Type: application/json" \74 -H "x-goog-api-key: $GEMINI_API_KEY" \75 -d '{76 "name": "MyBatchWebhook",77 "uri": "https://my-api.com/gemini-callback",78 "subscribed_events": ["batch.succeeded", "batch.failed"]79 }'8081For details on setting up your server to receive data, see the82[Handle webhook requests](https://ai.google.dev/gemini-api/docs/generate-content/webhooks#handle-webhook-requests) section.8384### Get a webhook8586Retrieve details about a specific webhook by its resource name.8788### Python8990 from google import genai9192 client = genai.Client()9394 webhook = client.webhooks.get(id="<your_webhook_id>")9596 print(f"Webhook: {webhook.name}")97 print(f"URI: {webhook.uri}")98 print(f"Events: {webhook.subscribed_events}")99100### JavaScript101102 import { GoogleGenAI } from "@google/genai";103104 const client = new GoogleGenAI(); // Assumes process.env.GEMINI_API_KEY is set105106 async function getWebhook() {107 const webhook = await client.webhooks.get("<your_webhook_id>");108109 console.log(`Webhook: ${webhook.name}`);110 console.log(`URI: ${webhook.uri}`);111 console.log(`Events: ${webhook.subscribed_events}`);112 }113114 getWebhook();115116### REST117118 curl -X GET \119 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \120 -H "x-goog-api-key: $GEMINI_API_KEY"121122### List webhooks123124List all configured webhooks for the current project, with optional pagination.125126### Python127128 from google import genai129130 client = genai.Client()131132 webhooks = client.webhooks.list()133134 for wh in webhooks:135 print(f"{wh.id}: {wh.name} -> {wh.uri}")136137### JavaScript138139 import { GoogleGenAI } from "@google/genai";140141 const client = new GoogleGenAI();142143 async function listWebhooks() {144 const webhooks = await client.webhooks.list();145146 for (const wh of webhooks) {147 console.log(`${wh.id}: ${wh.name} -> ${wh.uri}`);148 }149 }150151 listWebhooks();152153### REST154155 curl -X GET \156 "https://generativelanguage.googleapis.com/v1/webhooks" \157 -H "x-goog-api-key: $GEMINI_API_KEY"158159### Update a webhook160161Update an existing webhook's properties such as the display name, target URI, or162subscribed events.163164### Python165166 from google import genai167168 client = genai.Client()169170 updated_webhook = client.webhooks.update(171 id="<your_webhook_id>",172 subscribed_events=["batch.succeeded", "batch.failed", "batch.cancelled"],173 )174175 print(f"Updated webhook: {updated_webhook.name}")176177### JavaScript178179 import { GoogleGenAI } from "@google/genai";180181 const client = new GoogleGenAI();182183 async function updateWebhook() {184 const updatedWebhook = await client.webhooks.update(185 "<your_webhook_id>",186 {187 subscribed_events: ["batch.succeeded", "batch.failed", "batch.cancelled"],188 }189 );190191 console.log(`Updated webhook: ${updatedWebhook.name}`);192 }193194 updateWebhook();195196### REST197198 curl -X PATCH \199 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \200 -H "Content-Type: application/json" \201 -H "x-goog-api-key: $GEMINI_API_KEY" \202 -d '{203 "subscribed_events": ["batch.succeeded", "batch.failed", "batch.cancelled"]204 }'205206### Delete a webhook207208Remove a webhook endpoint from the project. This stops future event deliveries209to that endpoint.210211### Python212213 from google import genai214215 client = genai.Client()216217 client.webhooks.delete(id="<your_webhook_id>")218219 print("Webhook deleted.")220221### JavaScript222223 import { GoogleGenAI } from "@google/genai";224225 const client = new GoogleGenAI();226227 async function deleteWebhook() {228 await client.webhooks.delete("<your_webhook_id>");229230 console.log("Webhook deleted.");231 }232233 deleteWebhook();234235### REST236237 curl -X DELETE \238 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \239 -H "x-goog-api-key: $GEMINI_API_KEY"240241### Rotate a signing secret242243Rotate the signing secret for a webhook. You can configure whether previously244active secrets are revoked immediately or after a 24-hour grace period.245246**IMPORTANT** : The new signing secret is returned **only once** at rotation247time. Store it securely before updating your verification logic.248249### Python250251 from google import genai252 from google.genai import types253254 client = genai.Client()255256 response = client.webhooks.rotate_signing_secret(257 id="<your_webhook_id>",258 revocation_behavior="REVOKE_PREVIOUS_SECRETS_AFTER_H24",259 )260261 # Store response.secret securely, then update your server's verification config262 print("New signing secret generated. Update your server configuration.")263264### JavaScript265266 import { GoogleGenAI } from "@google/genai";267268 const client = new GoogleGenAI();269270 async function rotateSigningSecret() {271 const response = await client.webhooks.rotateSigningSecret(272 "<your_webhook_id>",273 {274 revocation_behavior: "REVOKE_PREVIOUS_SECRETS_AFTER_H24",275 }276 );277278 // Store response.secret securely, then update your server's verification config279 console.log("New signing secret generated. Update your server configuration.");280 }281282 rotateSigningSecret();283284### REST285286 curl -X POST \287 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>/rotate_secret" \288 -H "Content-Type: application/json" \289 -H "x-goog-api-key: $GEMINI_API_KEY" \290 -d '{291 "revocation_behavior": "REVOKE_PREVIOUS_SECRETS_AFTER_H24"292 }'293294### Handle webhook requests on a server295296When an event happens that you're subscribed to, your webhook URL will receive297an HTTP POST request. Your endpoint must respond with a 2xx status298code within a few seconds to avoid a retry. To ensure delivery, the Gemini API299automatically retries failed requests for 24 hours using exponential backoff.300301Gemini strictly follows the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification for302security headers. Verify the payload on your server using the signed header303signatures and your stored static signing secret. See the [Webhook envelope](https://ai.google.dev/gemini-api/docs/generate-content/webhooks#webhook-envelope) section for the payload information.304305Here is an example using Flask for the HTTP listener:306307### Python308309 # pip install flask standardwebhooks310 import os311 from flask import Flask, request, jsonify312 # Standard verification wrapper for Standard Webhook Headers313 from standardwebhooks.webhooks import Webhook, WebhookVerificationError314315 app = Flask(__name__)316317 SIGNING_SECRET = os.environ.get('WEBHOOK_SIGNING_SECRET')318319 @app.route('/gemini-callback', methods=['POST'])320 def gemini_callback():321 payload = request.get_data(as_text=True)322 headers = request.headers323324 try:325 wh = Webhook(SIGNING_SECRET)326 event = wh.verify(payload, headers)327 except WebhookVerificationError as e:328 return jsonify({"error": "Signature invalid"}), 400329330 # Process thin payload contents331 if event.get("type") == "batch.succeeded":332 print(f"Batch completed! ID: {event["data"]["id"]}")333 if event["data"].get("output_file_uri"):334 # For batch jobs with input file335 print(f"Batch file: {event["data"]["output_file_uri"]}")336 elif (event.type == "video.generated"):337 print(f"Video generated! URI: {event["data"]["output_file_uri"]}")338339 return jsonify({"status": "received"}), 200340341 if __name__ == "__main__":342 app.run(port=8000)343344### JavaScript345346 // npm install standardwebhooks347 import { Webhook } from "standardwebhooks";348 import express from "express";349350 const app = express();351 const client = new GoogleGenAI({ webhookSecret: process.env.WEBHOOK_SIGNING_SECRET });352353 // Don't use express.json() because signature verification needs the raw text body354 app.use(express.text({ type: "application/json" }));355356 app.post("/gemini-callback", async (req, res) => {357 const payload = await req.text();358 const headers: Record<string, string> = {};359 req.headers.forEach((value, key) => {360 headers[key] = value;361 });362363 try {364 const wh = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);365 const event = wh.verify(payload, headers) as Record<string, any>;366 console.log(`Event type: ${event.type}, data: ${JSON.stringify(event.data)}`);367368 // Process thin payload contents369 if (event.type === "batch.succeeded") {370 console.log(`Batch completed! ID: ${event.data.id}`);371 if (event.data.output_file_uri) {372 // For batch jobs with input file373 console.log(`Batch file: ${event.data.output_file_uri}`);374 }375 } else if (event.type === "video.generated") {376 console.log(`Video generated! URI: ${event.data.output_file_uri}`);377 }378379 res.status(200).json({ status: "received" });380 } catch (e) {381 console.error("Webhook verification failed:", e);382 res.status(400).send("Invalid signature");383 }384 });385386 app.listen(8000, () => {387 console.log("Webhook server is running on port 8000");388 });389390## Dynamic webhooks391392Dynamic webhooks allow you to bind a webhook endpoint to a **specific request393configuration**, ideal for agent-orchestration queues. Dynamic webhooks leverage394asymmetric public-key JWKS signatures instead of symmetric secrets.395396### Submit a dynamic request397398Add a `webhook_config` when triggering an asynchronous job (e.g., creating a399Batch).400401### Python402403 from google import genai404 from google.genai import types405406 client = genai.Client()407408 file_batch_job = client.batches.create(409 model="gemini-3.8-flash",410 src="files/uploaded_file_id",411 config={412 "display_name": "My Setup",413 "webhook_config": {414 "uris": ["https://my-api.com/gemini-webhook-dynamic"],415 "user_metadata":{"job_group": "nightly-eval", "priority": "high"}416 }417 }418 )419420### JavaScript421422 import { GoogleGenAI } from "@google/genai";423424 const client = new GoogleGenAI();425426 async function createBatchWithWebhook() {427 const fileBatchJob = await client.batches.create({428 model: "gemini-3.8-flash",429 src: "files/uploaded_file_id",430 config: {431 displayName: "My Setup",432 webhookConfig: {433 uris: ["https://my-api.com/gemini-webhook-dynamic"],434 user_metadata: {"job_group": "nightly-eval", "priority": "high"}435 },436 },437 });438 }439440### REST441442 curl -X POST \443 "https://generativelanguage.googleapis.com/v1/models/gemini-3.8-flash:batchCreate" \444 -H "Content-Type: application/json" \445 -H "x-goog-api-key: $GEMINI_API_KEY" \446 -d '{447 "src": "files/uploaded_file_id",448 "config": {449 "display_name": "My Setup",450 "webhook_config": {451 "uris": ["https://my-api.com/gemini-webhook-dynamic"],452 "user_metadata": {"job_group": "nightly-eval", "priority": "high"}453 }454 }455 }'456457### Verify dynamic signatures (JWKS)458459Dynamic webhook requests emit a JSON Web Token (JWT) signature. Your listener460must extract the signature and verify it using [Google's public certificate461endpoints](https://www.googleapis.com/oauth2/v3/certs).462463### Python464465 import jwt466 import requests467 from flask import Flask, request, jsonify468469 app = Flask(__name__)470471 # Google public cert list endpoint472 JWKS_URI = "https://generativelanguage.googleapis.com/.well-known/jwks.json"473474 def load_google_public_key(kid):475 response = requests.get(JWKS_URI).json()476 for key_item in response.get('keys', []):477 if key_item.get('kid') == kid:478 # Convert JWK to Cert wrapper479 return jwt.algorithms.RSAAlgorithm.from_jwk(key_item)480 return None481482 @app.route('/gemini-webhook-dynamic', methods=['POST'])483 def dynamic_handler():484 payload = request.get_data(as_text=True)485 headers = request.headers486487 token = headers.get('Webhook-Signature')488 if not token:489 return jsonify({"error": "No signature header"}), 400490491 try:492 # Extract kid from JWT header493 unverified_headers = jwt.get_unverified_header(token)494 pub_key = load_google_public_key(unverified_headers.get('kid'))495496 if not pub_key:497 return jsonify({"error": "Key cert not found"}), 400498499 # Verify Signature against expected audience (e.g., your project client ID)500 event = jwt.decode(501 token,502 pub_key,503 algorithms=["RS256"],504 audience="your-configured-audience"505 )506 except Exception as e:507 return jsonify({"error": "Invalid Dynamic signature", "details": str(e)}), 400508509 print("Verified Dynamic payload success.")510 return jsonify({"status": "received"}), 200511512### JavaScript513514 import { GoogleGenAI } from "@google/genai";515 import express from "express";516 import jwt from "jsonwebtoken";517 import jwksClient from "jwks-rsa";518519 const app = express();520 app.use(express.text({ type: 'application/json' }));521522 const client = jwksClient({523 jwksUri: "https://generativelanguage.googleapis.com/.well-known/jwks.json"524 });525526 function getKey(header, callback) {527 client.getSigningKey(header.kid, (err, key) => {528 const signingKey = key.getPublicKey();529 callback(null, signingKey);530 });531 }532533 app.post('/gemini-webhook-dynamic', (req, res) => {534 const token = req.headers['webhook-signature'];535536 if (!token) {537 return res.status(400).json({ error: "No signature header" });538 }539540 jwt.verify(541 token,542 getKey,543 {544 algorithms: ["RS256"],545 audience: "your-configured-audience"546 },547 (err, decoded) => {548 if (err) {549 return res.status(400).json({ error: "Invalid Dynamic signature", details: err.message });550 }551552 console.log("Verified Dynamic payload success.");553 res.status(200).json({ status: "received" });554 }555 );556 });557558## Webhook envelope559560To avoid bandwidth congestion, Gemini webhooks use a **thin payload** model to561deliver data.562deliveries send a snapshot containing status details and pointers to results,563rather than the raw output file itself.564565Here is an example payload format:566567 {568 "type": "batch.succeeded",569 "version": "v1",570 "timestamp": "2026-01-22T12:00:00Z",571 "data": {572 "id": "batch_123456",573 "output_file_uri": "gs://my-bucket/results.jsonl"574 }575 }576577## Event catalog reference578579The following events are triggered for supporting jobs:580581| Event type | Trigger | Payload item (`data`) |582|---|---|---|583| `batch.succeeded` | Processing finished successfully. | `id`, `output_file_uri` |584| `batch.cancelled` | User cancelled request | `id` |585| `batch.expired` | Batch has not been processed (finished) in 24h timeframe | `id` |586| `batch.failed` | Batch job failed (system or validation error). | `id`, `error_code`, `error_message` |587| `interaction.requires_action` | Function call, user needs to do something | `id` |588| `interaction.completed` | LRO in interactions API succeeded | `id` |589| `interaction.failed` | LRO in interactions API failed (system or validation error). | `id`, `error_code`, `error_message` |590| `interaction.cancelled` | LRO in interactions API cancelled | `id` |591| `video.generated` | Video generation LRO completed. | `id`, `output_file_uri`, `file_name` |592593## Best practices594595To ensure reliable, scalable operation:596597- **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).598- **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.599- **Deduplication handling** : Standard webhooks deliver "At-least-once". Use the consistent `webhook-id` header to handle potential duplicates in higher congestion flows.600601## What's next?602603- [Batch API](https://ai.google.dev/gemini-api/docs/batch): Utilize webhooks to automate high volume endpoints.