SPB Git

Repository Public

webhooks.md 32.7 KB 944 lines markdown

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.
Files main
Menu

Theme