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/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/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/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### Java7071 import com.google.genai.Client;72 import com.google.genai.gaos.models.webhooks.Webhook;73 import com.google.genai.gaos.models.webhooks.WebhookInput;74 import com.google.genai.gaos.models.webhooks.WebhookSubscribedEvent;75 import java.util.Arrays;7677 Client client = new Client();7879 WebhookInput input =80 WebhookInput.builder()81 .name("MyBatchWebhook")82 .subscribedEvents(83 Arrays.asList(84 WebhookSubscribedEvent.BATCH_SUCCEEDED, WebhookSubscribedEvent.BATCH_FAILED))85 .uri("https://my-api.com/gemini-callback")86 .build();8788 Webhook webhook = client.webhooks.create(input).webhook().get();8990 // Store webhook.newSigningSecret() securely91 String webhookSecret = webhook.newSigningSecret().orElse("");92 System.out.println(93 "Created webhook: " + webhook.name().orElse("") + ", " + webhook.id().orElse(""));9495### REST9697 curl -X POST \98 "https://generativelanguage.googleapis.com/v1/webhooks" \99 -H "Content-Type: application/json" \100 -H "x-goog-api-key: $GEMINI_API_KEY" \101 -d '{102 "name": "MyBatchWebhook",103 "uri": "https://my-api.com/gemini-callback",104 "subscribed_events": ["batch.succeeded", "batch.failed"]105 }'106107For details on setting up your server to receive data, see the108[Handle webhook requests](https://ai.google.dev/gemini-api/docs/webhooks#handle-webhook-requests) section.109110### Get a webhook111112Retrieve details about a specific webhook by its resource name.113114### Python115116 from google import genai117118 client = genai.Client()119120 webhook = client.webhooks.get(id="<your_webhook_id>")121122 print(f"Webhook: {webhook.name}")123 print(f"URI: {webhook.uri}")124 print(f"Events: {webhook.subscribed_events}")125126### JavaScript127128 import { GoogleGenAI } from "@google/genai";129130 const client = new GoogleGenAI(); // Assumes process.env.GEMINI_API_KEY is set131132 async function getWebhook() {133 const webhook = await client.webhooks.get("<your_webhook_id>");134135 console.log(`Webhook: ${webhook.name}`);136 console.log(`URI: ${webhook.uri}`);137 console.log(`Events: ${webhook.subscribed_events}`);138 }139140 getWebhook();141142### Java143144 import com.google.genai.Client;145 import com.google.genai.gaos.models.webhooks.Webhook;146 import java.util.Collections;147148 Client client = new Client();149150 Webhook webhook = client.webhooks.get("<your_webhook_id>").webhook().get();151152 System.out.println("Webhook: " + webhook.name().orElse(""));153 System.out.println("URI: " + webhook.uri().orElse(""));154 System.out.println("Events: " + webhook.subscribedEvents().orElse(Collections.emptyList()));155156### REST157158 curl -X GET \159 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \160 -H "x-goog-api-key: $GEMINI_API_KEY"161162### List webhooks163164List all configured webhooks for the current project, with optional pagination.165166### Python167168 from google import genai169170 client = genai.Client()171172 webhooks = client.webhooks.list()173174 for wh in webhooks:175 print(f"{wh.id}: {wh.name} -> {wh.uri}")176177### JavaScript178179 import { GoogleGenAI } from "@google/genai";180181 const client = new GoogleGenAI();182183 async function listWebhooks() {184 const webhooks = await client.webhooks.list();185186 for (const wh of webhooks) {187 console.log(`${wh.id}: ${wh.name} -> ${wh.uri}`);188 }189 }190191 listWebhooks();192193### Java194195 import com.google.genai.Client;196 import com.google.genai.gaos.models.webhooks.Webhook;197 import com.google.genai.gaos.models.webhooks.WebhookListResponse;198 import java.util.Collections;199200 Client client = new Client();201202 WebhookListResponse response =203 client.webhooks.listDirect().webhookListResponse().orElse(new WebhookListResponse());204205 for (Webhook wh : response.webhooks().orElse(Collections.emptyList())) {206 System.out.println(207 wh.id().orElse("") + ": " + wh.name().orElse("") + " -> " + wh.uri().orElse(""));208 }209210### REST211212 curl -X GET \213 "https://generativelanguage.googleapis.com/v1/webhooks" \214 -H "x-goog-api-key: $GEMINI_API_KEY"215216### Update a webhook217218Update an existing webhook's properties such as the display name, target URI, or219subscribed events.220221### Python222223 from google import genai224225 client = genai.Client()226227 updated_webhook = client.webhooks.update(228 id="<your_webhook_id>",229 subscribed_events=["batch.succeeded", "batch.failed", "batch.cancelled"],230 )231232 print(f"Updated webhook: {updated_webhook.name}")233234### JavaScript235236 import { GoogleGenAI } from "@google/genai";237238 const client = new GoogleGenAI();239240 async function updateWebhook() {241 const updatedWebhook = await client.webhooks.update(242 "<your_webhook_id>",243 {244 subscribed_events: ["batch.succeeded", "batch.failed", "batch.cancelled"],245 }246 );247248 console.log(`Updated webhook: ${updatedWebhook.name}`);249 }250251 updateWebhook();252253### Java254255 import com.google.genai.Client;256 import com.google.genai.gaos.models.webhooks.Webhook;257 import com.google.genai.gaos.models.webhooks.WebhookUpdate;258 import com.google.genai.gaos.models.webhooks.WebhookUpdateSubscribedEvent;259 import java.util.Arrays;260261 Client client = new Client();262263 WebhookUpdate updateBody =264 WebhookUpdate.builder()265 .subscribedEvents(266 Arrays.asList(267 WebhookUpdateSubscribedEvent.BATCH_SUCCEEDED,268 WebhookUpdateSubscribedEvent.BATCH_FAILED,269 WebhookUpdateSubscribedEvent.of("batch.cancelled")))270 .build();271272 Webhook updatedWebhook =273 client.webhooks274 .update()275 .id("<your_webhook_id>")276 .updateMask("subscribed_events")277 .body(updateBody)278 .call()279 .webhook()280 .get();281282 System.out.println("Updated webhook: " + updatedWebhook.name().orElse(""));283284### REST285286 curl -X PATCH \287 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \288 -H "Content-Type: application/json" \289 -H "x-goog-api-key: $GEMINI_API_KEY" \290 -d '{291 "subscribed_events": ["batch.succeeded", "batch.failed", "batch.cancelled"]292 }'293294### Delete a webhook295296Remove a webhook endpoint from the project. This stops future event deliveries297to that endpoint.298299### Python300301 from google import genai302303 client = genai.Client()304305 client.webhooks.delete(id="<your_webhook_id>")306307 print("Webhook deleted.")308309### JavaScript310311 import { GoogleGenAI } from "@google/genai";312313 const client = new GoogleGenAI();314315 async function deleteWebhook() {316 await client.webhooks.delete("<your_webhook_id>");317318 console.log("Webhook deleted.");319 }320321 deleteWebhook();322323### Java324325 import com.google.genai.Client;326327 Client client = new Client();328329 client.webhooks.delete("<your_webhook_id>");330331 System.out.println("Webhook deleted.");332333### REST334335 curl -X DELETE \336 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \337 -H "x-goog-api-key: $GEMINI_API_KEY"338339### Rotate a signing secret340341Rotate the signing secret for a webhook. You can configure whether previously342active secrets are revoked immediately or after a 24-hour grace period.343344**IMPORTANT** : The new signing secret is returned **only once** at rotation345time. Store it securely before updating your verification logic.346347### Python348349 from google import genai350 from google.genai import types351352 client = genai.Client()353354 response = client.webhooks.rotate_signing_secret(355 id="<your_webhook_id>",356 revocation_behavior="REVOKE_PREVIOUS_SECRETS_AFTER_H24",357 )358359 # Store response.secret securely, then update your server's verification config360 print("New signing secret generated. Update your server configuration.")361362### JavaScript363364 import { GoogleGenAI } from "@google/genai";365366 const client = new GoogleGenAI();367368 async function rotateSigningSecret() {369 const response = await client.webhooks.rotateSigningSecret(370 "<your_webhook_id>",371 {372 revocation_behavior: "REVOKE_PREVIOUS_SECRETS_AFTER_H24",373 }374 );375376 // Store response.secret securely, then update your server's verification config377 console.log("New signing secret generated. Update your server configuration.");378 }379380 rotateSigningSecret();381382### Java383384 import com.google.genai.Client;385 import com.google.genai.gaos.models.webhooks.RevocationBehavior;386 import com.google.genai.gaos.models.webhooks.RotateSigningSecretRequest;387 import com.google.genai.gaos.models.webhooks.WebhookRotateSigningSecretResponse;388389 Client client = new Client();390391 RotateSigningSecretRequest requestBody =392 RotateSigningSecretRequest.builder()393 .revocationBehavior(RevocationBehavior.REVOKE_PREVIOUS_SECRETS_AFTER_H24)394 .build();395396 WebhookRotateSigningSecretResponse response =397 client.webhooks398 .rotateSigningSecret()399 .id("<your_webhook_id>")400 .body(requestBody)401 .call()402 .webhookRotateSigningSecretResponse()403 .get();404405 // Store response.secret() securely, then update your server's verification config406 String newSecret = response.secret().orElse("");407 System.out.println("New signing secret generated. Update your server configuration.");408409### REST410411 curl -X POST \412 "https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>/rotate_secret" \413 -H "Content-Type: application/json" \414 -H "x-goog-api-key: $GEMINI_API_KEY" \415 -d '{416 "revocation_behavior": "REVOKE_PREVIOUS_SECRETS_AFTER_H24"417 }'418419### Handle webhook requests on a server420421When an event happens that you're subscribed to, your webhook URL will receive422an HTTP POST request. Your endpoint must respond with a 2xx status423code within a few seconds to avoid a retry. To ensure delivery, the Gemini API424automatically retries failed requests for 24 hours using exponential backoff.425426Gemini strictly follows the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification for427security headers. Verify the payload on your server using the signed header428signatures and your stored static signing secret. See the [Webhook envelope](https://ai.google.dev/gemini-api/docs/webhooks#webhook-envelope) section for the payload information.429430Here is an example using Flask for the HTTP listener:431432### Python433434 # pip install flask standardwebhooks435 import os436 from flask import Flask, request, jsonify437 # Standard verification wrapper for Standard Webhook Headers438 from standardwebhooks.webhooks import Webhook, WebhookVerificationError439440 app = Flask(__name__)441442 SIGNING_SECRET = os.environ.get('WEBHOOK_SIGNING_SECRET')443444 @app.route('/gemini-callback', methods=['POST'])445 def gemini_callback():446 payload = request.get_data(as_text=True)447 headers = request.headers448449 try:450 wh = Webhook(SIGNING_SECRET)451 event = wh.verify(payload, headers)452 except WebhookVerificationError as e:453 return jsonify({"error": "Signature invalid"}), 400454455 # Process thin payload contents456 if event.get("type") == "batch.succeeded":457 print(f"Batch completed! ID: {event['data']['id']}")458 if event["data"].get("output_file_uri"):459 # For batch jobs with input file460 print(f"Batch file: {event['data']['output_file_uri']}")461 elif event.get("type") == "interaction.completed":462 print(f"Interaction completed! ID: {event['data']['id']}")463 elif event.get("type") == "video.generated":464 print(f"Video generated! URI: {event['data']['output_file_uri']}")465466 return jsonify({"status": "received"}), 200467468 if __name__ == "__main__":469 app.run(port=8000)470471### JavaScript472473 // npm install standardwebhooks474 import { Webhook } from "standardwebhooks";475 import express from "express";476477 const app = express();478 const client = new GoogleGenAI({ webhookSecret: process.env.WEBHOOK_SIGNING_SECRET });479480 // Don't use express.json() because signature verification needs the raw text body481 app.use(express.text({ type: "application/json" }));482483 app.post("/gemini-callback", async (req, res) => {484 const payload = await req.text();485 const headers: Record<string, string> = {};486 req.headers.forEach((value, key) => {487 headers[key] = value;488 });489490 try {491 const wh = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);492 const event = wh.verify(payload, headers) as Record<string, any>;493 console.log(`Event type: ${event.type}, data: ${JSON.stringify(event.data)}`);494495 // Process thin payload contents496 if (event.type === "batch.succeeded") {497 console.log(`Batch completed! ID: ${event.data.id}`);498 if (event.data.output_file_uri) {499 // For batch jobs with input file500 console.log(`Batch file: ${event.data.output_file_uri}`);501 }502 } else if (event.type === "interaction.completed") {503 console.log(`Interaction completed! ID: ${event.data.id}`);504 } else if (event.type === "video.generated") {505 console.log(`Video generated! URI: ${event.data.output_file_uri}`);506 }507508 res.status(200).json({ status: "received" });509 } catch (e) {510 console.error("Webhook verification failed:", e);511 res.status(400).send("Invalid signature");512 }513 });514515 app.listen(8000, () => {516 console.log("Webhook server is running on port 8000");517 });518519### Java520521 import com.sun.net.httpserver.HttpServer;522 import java.io.OutputStream;523 import java.net.InetSocketAddress;524 import java.nio.charset.StandardCharsets;525 import java.util.Base64;526 import java.util.regex.Matcher;527 import java.util.regex.Pattern;528 import javax.crypto.Mac;529 import javax.crypto.spec.SecretKeySpec;530531 String signingSecret = System.getenv("WEBHOOK_SIGNING_SECRET");532533 HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);534 server.createContext(535 "/gemini-callback",536 exchange -> {537 String payload =538 new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);539 String msgId = exchange.getRequestHeaders().getFirst("webhook-id");540 String msgTimestamp = exchange.getRequestHeaders().getFirst("webhook-timestamp");541 String msgSignature = exchange.getRequestHeaders().getFirst("webhook-signature");542543 try {544 String toSign = msgId + "." + msgTimestamp + "." + payload;545 Mac mac = Mac.getInstance("HmacSHA256");546 byte[] secretBytes =547 Base64.getDecoder().decode(signingSecret.replaceFirst("^whsec_", ""));548 mac.init(new SecretKeySpec(secretBytes, "HmacSHA256"));549 String expectedSig =550 "v1,"551 + Base64.getEncoder()552 .encodeToString(mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)));553554 if (msgSignature == null || !msgSignature.contains(expectedSig)) {555 byte[] resp = "{\"error\": \"Signature invalid\"}".getBytes(StandardCharsets.UTF_8);556 exchange.sendResponseHeaders(400, resp.length);557 try (OutputStream os = exchange.getResponseBody()) {558 os.write(resp);559 }560 return;561 }562563 Matcher typeMatcher = Pattern.compile("\"type\"\\s*:\\s*\"([^\"]+)\"").matcher(payload);564 String type = typeMatcher.find() ? typeMatcher.group(1) : "";565566 Matcher idMatcher = Pattern.compile("\"id\"\\s*:\\s*\"([^\"]+)\"").matcher(payload);567 String id = idMatcher.find() ? idMatcher.group(1) : "";568569 Matcher uriMatcher =570 Pattern.compile("\"output_file_uri\"\\s*:\\s*\"([^\"]+)\"").matcher(payload);571 String outputFileUri = uriMatcher.find() ? uriMatcher.group(1) : "";572573 if ("batch.succeeded".equals(type)) {574 System.out.println("Batch completed! ID: " + id);575 if (!outputFileUri.isEmpty()) {576 System.out.println("Batch file: " + outputFileUri);577 }578 } else if ("interaction.completed".equals(type)) {579 System.out.println("Interaction completed! ID: " + id);580 } else if ("video.generated".equals(type)) {581 System.out.println("Video generated! URI: " + outputFileUri);582 }583584 byte[] resp = "{\"status\": \"received\"}".getBytes(StandardCharsets.UTF_8);585 exchange.sendResponseHeaders(200, resp.length);586 try (OutputStream os = exchange.getResponseBody()) {587 os.write(resp);588 }589 } catch (Exception e) {590 exchange.sendResponseHeaders(400, -1);591 }592 });593 server.start();594595## Dynamic webhooks596597Dynamic webhooks allow you to bind a webhook endpoint to a **specific request598configuration**, ideal for agent-orchestration queues. Dynamic webhooks leverage599asymmetric public-key JWKS signatures instead of symmetric secrets.600601### Submit a dynamic request602603Add a `webhook_config` when triggering an asynchronous job (e.g., creating a604Batch).605606### Python607608 # This will only work for SDK newer than 2.0.0609 from google import genai610611 client = genai.Client()612613 response = client.interactions.create(614 model='gemini-3.8-flash',615 input='Tell me a short joke about programming.',616 background=True, # Required when webhook_config is specified617 webhook_config={618 'uris': ["https://my-api.com/gemini-webhook-dynamic"],619 'user_metadata': {"job_group": "nightly-eval", "priority": "high"}620 }621 )622623 print(f"Interaction created! ID: {response.id}")624 print(f"Status: {response.status}")625626### JavaScript627628 // This will only work for SDK newer than 2.0.0629 import { GoogleGenAI } from "@google/genai";630631 const client = new GoogleGenAI();632633 async function createInteractionWithWebhook() {634 const response = await client.interactions.create({635 model: "gemini-3.8-flash",636 input: "Tell me a short joke about programming.",637 background: true, // Required when webhook_config is specified638 webhook_config: {639 uris: ["https://my-api.com/gemini-webhook-dynamic"],640 user_metadata: { job_group: "nightly-eval", priority: "high" },641 },642 });643644 console.log(`Interaction created! ID: ${response.id}`);645 console.log(`Status: ${response.status}`);646 }647648 createInteractionWithWebhook();649650### Java651652 import com.google.genai.Client;653 import com.google.genai.gaos.models.interactions.CreateModelInteraction;654 import com.google.genai.gaos.models.interactions.Interaction;655 import com.google.genai.gaos.models.interactions.InteractionStatus;656 import com.google.genai.gaos.models.interactions.InteractionsInput;657 import com.google.genai.gaos.models.interactions.WebhookConfig;658 import com.google.genai.gaos.models.operations.CreateInteractionRequestBody;659 import java.util.Arrays;660 import java.util.HashMap;661 import java.util.Map;662663 Client client = new Client();664665 Map<String, Object> userMetadata = new HashMap<>();666 userMetadata.put("job_group", "nightly-eval");667 userMetadata.put("priority", "high");668669 WebhookConfig webhookConfig =670 WebhookConfig.builder()671 .uris(Arrays.asList("https://my-api.com/gemini-webhook-dynamic"))672 .userMetadata(userMetadata)673 .build();674675 CreateModelInteraction params =676 CreateModelInteraction.builder()677 .model("gemini-3.8-flash")678 .input(InteractionsInput.of("Tell me a short joke about programming."))679 .background(true) // Required when webhookConfig is specified680 .webhookConfig(webhookConfig)681 .build();682683 Interaction response =684 client.interactions.create(CreateInteractionRequestBody.of(params)).interaction().get();685686 System.out.println("Interaction created! ID: " + response.id().orElse(""));687 System.out.println(688 "Status: " + response.status().map(InteractionStatus::value).orElse(""));689690### REST691692 # Specifies the API revision to avoid breaking changes when they become default693 curl -X POST \694 "https://generativelanguage.googleapis.com/v1beta/interactions" \695 -H "Content-Type: application/json" \696 -H "x-goog-api-key: $GEMINI_API_KEY" \697 -d '{698 "model": "gemini-3.8-flash",699 "input": "Tell me a short joke about programming.",700 "background": true,701 "webhook_config": {702 "uris": ["https://my-api.com/gemini-webhook-dynamic"],703 "user_metadata": {"job_group": "nightly-eval", "priority": "high"}704 }705 }'706707### Verify dynamic signatures (JWKS)708709Dynamic webhook requests emit a JSON Web Token (JWT) signature. Your listener710must extract the signature and verify it using [Google's public certificate711endpoints](https://www.googleapis.com/oauth2/v3/certs).712713### Python714715 import jwt716 import requests717 from flask import Flask, request, jsonify718719 app = Flask(__name__)720721 # Google public cert list endpoint722 JWKS_URI = "https://generativelanguage.googleapis.com/.well-known/jwks.json"723724 def load_google_public_key(kid):725 response = requests.get(JWKS_URI).json()726 for key_item in response.get('keys', []):727 if key_item.get('kid') == kid:728 # Convert JWK to Cert wrapper729 return jwt.algorithms.RSAAlgorithm.from_jwk(key_item)730 return None731732 @app.route('/gemini-webhook-dynamic', methods=['POST'])733 def dynamic_handler():734 payload = request.get_data(as_text=True)735 headers = request.headers736737 token = headers.get('Webhook-Signature')738 if not token:739 return jsonify({"error": "No signature header"}), 400740741 try:742 # Extract kid from JWT header743 unverified_headers = jwt.get_unverified_header(token)744 pub_key = load_google_public_key(unverified_headers.get('kid'))745746 if not pub_key:747 return jsonify({"error": "Key cert not found"}), 400748749 # Verify Signature against expected audience (e.g., your project client ID)750 event = jwt.decode(751 token,752 pub_key,753 algorithms=["RS256"],754 audience="your-configured-audience"755 )756 except Exception as e:757 return jsonify({"error": "Invalid Dynamic signature", "details": str(e)}), 400758759 print("Verified Dynamic payload success.")760 return jsonify({"status": "received"}), 200761762### JavaScript763764 import { GoogleGenAI } from "@google/genai";765 import express from "express";766 import jwt from "jsonwebtoken";767 import jwksClient from "jwks-rsa";768769 const app = express();770 app.use(express.text({ type: 'application/json' }));771772 const client = jwksClient({773 jwksUri: "https://generativelanguage.googleapis.com/.well-known/jwks.json"774 });775776 function getKey(header, callback) {777 client.getSigningKey(header.kid, (err, key) => {778 const signingKey = key.getPublicKey();779 callback(null, signingKey);780 });781 }782783 app.post('/gemini-webhook-dynamic', (req, res) => {784 const token = req.headers['webhook-signature'];785786 if (!token) {787 return res.status(400).json({ error: "No signature header" });788 }789790 jwt.verify(791 token,792 getKey,793 {794 algorithms: ["RS256"],795 audience: "your-configured-audience"796 },797 (err, decoded) => {798 if (err) {799 return res.status(400).json({ error: "Invalid Dynamic signature", details: err.message });800 }801802 console.log("Verified Dynamic payload success.");803 res.status(200).json({ status: "received" });804 }805 );806 });807808### Java809810 import com.sun.net.httpserver.HttpServer;811 import java.io.InputStream;812 import java.io.OutputStream;813 import java.math.BigInteger;814 import java.net.InetSocketAddress;815 import java.net.URI;816 import java.nio.charset.StandardCharsets;817 import java.security.KeyFactory;818 import java.security.PublicKey;819 import java.security.Signature;820 import java.security.spec.RSAPublicKeySpec;821 import java.util.Base64;822 import java.util.regex.Matcher;823 import java.util.regex.Pattern;824825 String jwksUri = "https://generativelanguage.googleapis.com/.well-known/jwks.json";826827 HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);828 server.createContext(829 "/gemini-webhook-dynamic",830 exchange -> {831 String token = exchange.getRequestHeaders().getFirst("Webhook-Signature");832 if (token == null || token.split("\\.").length != 3) {833 byte[] resp = "{\"error\": \"No signature header\"}".getBytes(StandardCharsets.UTF_8);834 exchange.sendResponseHeaders(400, resp.length);835 try (OutputStream os = exchange.getResponseBody()) {836 os.write(resp);837 }838 return;839 }840841 try {842 String[] parts = token.split("\\.");843 String headerJson =844 new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);845 Matcher kidMatcher = Pattern.compile("\"kid\"\\s*:\\s*\"([^\"]+)\"").matcher(headerJson);846 String kid = kidMatcher.find() ? kidMatcher.group(1) : "";847848 PublicKey pubKey = null;849 try (InputStream in = URI.create(jwksUri).toURL().openStream()) {850 String jwksJson = new String(in.readAllBytes(), StandardCharsets.UTF_8);851 Matcher keyBlockMatcher =852 Pattern.compile(853 "\\{[^}]*\"kid\"\\s*:\\s*\"" + Pattern.quote(kid) + "\"[^}]*\\}")854 .matcher(jwksJson);855 if (keyBlockMatcher.find()) {856 String keyBlock = keyBlockMatcher.group(0);857 Matcher nMatcher = Pattern.compile("\"n\"\\s*:\\s*\"([^\"]+)\"").matcher(keyBlock);858 Matcher eMatcher = Pattern.compile("\"e\"\\s*:\\s*\"([^\"]+)\"").matcher(keyBlock);859 if (nMatcher.find() && eMatcher.find()) {860 BigInteger n =861 new BigInteger(1, Base64.getUrlDecoder().decode(nMatcher.group(1)));862 BigInteger e =863 new BigInteger(1, Base64.getUrlDecoder().decode(eMatcher.group(1)));864 pubKey =865 KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e));866 }867 }868 }869870 Signature sig = Signature.getInstance("SHA256withRSA");871 sig.initVerify(pubKey);872 sig.update((parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8));873 boolean verified = sig.verify(Base64.getUrlDecoder().decode(parts[2]));874875 if (!verified) {876 throw new SecurityException("Signature verification failed");877 }878879 System.out.println("Verified Dynamic payload success.");880 byte[] resp = "{\"status\": \"received\"}".getBytes(StandardCharsets.UTF_8);881 exchange.sendResponseHeaders(200, resp.length);882 try (OutputStream os = exchange.getResponseBody()) {883 os.write(resp);884 }885 } catch (Exception e) {886 byte[] resp =887 ("{\"error\": \"Invalid Dynamic signature\", \"details\": \""888 + e.getMessage()889 + "\"}")890 .getBytes(StandardCharsets.UTF_8);891 exchange.sendResponseHeaders(400, resp.length);892 try (OutputStream os = exchange.getResponseBody()) {893 os.write(resp);894 }895 }896 });897 server.start();898899## Webhook envelope900901To avoid bandwidth congestion, Gemini webhooks use a **thin payload** model to902deliver data.903deliveries send a snapshot containing status details and pointers to results,904rather than the raw output file itself.905906Here is an example payload format:907908 {909 "type": "batch.succeeded",910 "version": "v1",911 "timestamp": "2026-01-22T12:00:00Z",912 "data": {913 "id": "batch_123456",914 "output_file_uri": "gs://my-bucket/results.jsonl"915 }916 }917918## Event catalog reference919920The following events are triggered for supporting jobs:921922| Event type | Trigger | Payload item (`data`) |923|---|---|---|924| `batch.succeeded` | Processing finished successfully. | `id`, `output_file_uri` |925| `batch.cancelled` | User cancelled request | `id` |926| `batch.expired` | Batch has not been processed (finished) in 24h timeframe | `id` |927| `batch.failed` | Batch job failed (system or validation error). | `id`, `error_code`, `error_message` |928| `interaction.requires_action` | Function call, user needs to do something | `id` |929| `interaction.completed` | LRO in interactions API succeeded | `id` |930| `interaction.failed` | LRO in interactions API failed (system or validation error). | `id`, `error_code`, `error_message` |931| `interaction.cancelled` | LRO in interactions API cancelled | `id` |932| `video.generated` | Video generation LRO completed. | `id`, `output_file_uri`, `file_name` |933934## Best practices935936To ensure reliable, scalable operation:937938- **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).939- **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.940- **Deduplication handling** : Standard webhooks deliver "At-least-once". Use the consistent `webhook-id` header to handle potential duplicates in higher congestion flows.941942## What's next?943944- [Batch API](https://ai.google.dev/gemini-api/docs/batch-api): Utilize webhooks to automate high volume endpoints.