SPB Git

Repository Public

webhooks.md 19.3 KB 603 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/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.
Files main
Menu

Theme