SPB Git

Repository Public

webhooks.md 20.3 KB 619 lines markdown

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

Theme