Los webhooks permiten que la API de Gemini envíe notificaciones en tiempo real a tu servidor cuando se completan las operaciones asíncronas o de larga duración (LRO). Esto reemplaza la necesidad de sondear la API para obtener actualizaciones de estado, lo que reduce la latencia y la sobrecarga.
Los webhooks están disponibles para operaciones como trabajos por lotes, interacciones y generación de videos.
Cómo funciona
En lugar de sondear GET /operations repetidamente para verificar si un trabajo finalizó, puedes configurar los webhooks de la API de Gemini para que envíen una solicitud HTTP POST a tu URL de escucha inmediatamente después de que se active un evento.
La API de Gemini admite dos formas de configurar webhooks:
- Webhooks estáticos: Son extremos a nivel del proyecto configurados con la API de WebhookService de Gemini. Es útil para las integraciones globales (p. ej., notificar a Slack, sincronizar una base de datos, etcétera).
- Webhooks dinámicos: Son anulaciones a nivel de la solicitud que pasan una URL de webhook en la carga útil de configuración de una llamada a trabajos específica. Es ideal para enrutar trabajos específicos a extremos dedicados.
Webhooks estáticos
Los webhooks estáticos se registran para todo un proyecto y se activan para cualquier evento coincidente.
Crea un webhook
Puedes crear extremos con el SDK o la API de REST.
IMPORTANTE: Cuando creas un webhook, la API devuelve un secreto de firma solo una vez. Debes almacenar esta clave de forma segura (p.ej., en tus variables de entorno) para verificar las firmas más adelante. Si pierdes el secreto de firma, deberás rotarlo.
Python
from google import genai
client = genai.Client()
webhook = client.webhooks.create(
name="MyBatchWebhook",
subscribed_events=["batch.completed", "batch.failed"],
uri="https://my-api.com/gemini-callback",
)
# Store webhook.new_signing_secret securely
webhook_secret = webhook.new_signing_secret
print(f"Created webhook: {webhook.name}, {webhook.id}")
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI();
async function createWebhook() {
const webhook = await client.webhooks.create({
name: "MyBatchWebhook",
subscribed_events: ["batch.completed", "batch.failed"],
uri: "https://my-api.com/gemini-callback",
});
// Store webhook.signingSecret securely
const webhookSecret = webhook.new_signing_secret;
console.log(`Created webhook: ${webhook.name}, ${webhook.id}`);
}
createWebhook();
REST
curl -X POST \
"https://generativelanguage.googleapis.com/v1/webhooks" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GOOGLE_API_KEY" \
-d '{
"name": "MyBatchWebhook",
"uri": "https://my-api.com/gemini-callback",
"subscribed_events": ["batch.completed", "batch.failed"]
}'
Para obtener detalles sobre cómo configurar tu servidor para recibir datos, consulta la sección Cómo controlar solicitudes de webhook.
Obtén un webhook
Recupera detalles sobre un webhook específico por su nombre de recurso.
Python
from google import genai
client = genai.Client()
webhook = client.webhooks.get(id="<your_webhook_id>")
print(f"Webhook: {webhook.name}")
print(f"URI: {webhook.uri}")
print(f"Events: {webhook.subscribed_events}")
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI(); // Assumes process.env.GEMINI_API_KEY is set
async function getWebhook() {
const webhook = await client.webhooks.get("<your_webhook_id>");
console.log(`Webhook: ${webhook.name}`);
console.log(`URI: ${webhook.uri}`);
console.log(`Events: ${webhook.subscribed_events}`);
}
getWebhook();
REST
curl -X GET \
"https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \
-H "x-goog-api-key: $GOOGLE_API_KEY"
Enumera webhooks
Enumera todos los webhooks configurados para el proyecto actual, con paginación opcional.
Python
from google import genai
client = genai.Client()
webhooks = client.webhooks.list()
for wh in webhooks:
print(f"{wh.id}: {wh.name} -> {wh.uri}")
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI();
async function listWebhooks() {
const webhooks = await client.webhooks.list();
for (const wh of webhooks) {
console.log(`${wh.id}: ${wh.name} -> ${wh.uri}`);
}
}
listWebhooks();
REST
curl -X GET \
"https://generativelanguage.googleapis.com/v1/webhooks" \
-H "x-goog-api-key: $GOOGLE_API_KEY"
Actualiza un webhook
Actualiza las propiedades de un webhook existente, como el nombre visible, el URI de destino o los eventos suscritos.
Python
from google import genai
client = genai.Client()
updated_webhook = client.webhooks.update(
id="<your_webhook_id>",
subscribed_events=["batch.completed", "batch.failed", "batch.cancelled"],
)
print(f"Updated webhook: {updated_webhook.name}")
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI();
async function updateWebhook() {
const updatedWebhook = await client.webhooks.update(
"<your_webhook_id>",
{
subscribed_events: ["batch.completed", "batch.failed", "batch.cancelled"],
}
);
console.log(`Updated webhook: ${updatedWebhook.name}`);
}
updateWebhook();
REST
curl -X PATCH \
"https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GOOGLE_API_KEY" \
-d '{
"subscribed_events": ["batch.completed", "batch.failed", "batch.cancelled"]
}'
Cómo borrar un webhook
Quita un extremo de webhook del proyecto. De esta manera, se detiene la entrega de eventos futuros a ese extremo.
Python
from google import genai
client = genai.Client()
client.webhooks.delete(id="<your_webhook_id>")
print("Webhook deleted.")
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI();
async function deleteWebhook() {
await client.webhooks.delete("<your_webhook_id>");
console.log("Webhook deleted.");
}
deleteWebhook();
REST
curl -X DELETE \
"https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>" \
-H "x-goog-api-key: $GOOGLE_API_KEY"
Rota un secreto de firma
Rota el secreto de firma de un webhook. Puedes configurar si los secretos que estaban activos anteriormente se revocan de inmediato o después de un período de gracia de 24 horas.
IMPORTANTE: El nuevo secreto de firma se devuelve solo una vez en el momento de la rotación. Almacénalo de forma segura antes de actualizar la lógica de verificación.
Python
from google import genai
from google.genai import types
client = genai.Client()
response = client.webhooks.rotate_signing_secret(
id="<your_webhook_id>",
revocation_behavior="REVOKE_PREVIOUS_SECRETS_AFTER_H24",
)
# Store response.secret securely, then update your server's verification config
print("New signing secret generated. Update your server configuration.")
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI();
async function rotateSigningSecret() {
const response = await client.webhooks.rotateSigningSecret(
"<your_webhook_id>",
{
revocation_behavior: "REVOKE_PREVIOUS_SECRETS_AFTER_H24",
}
);
// Store response.secret securely, then update your server's verification config
console.log("New signing secret generated. Update your server configuration.");
}
rotateSigningSecret();
REST
curl -X POST \
"https://generativelanguage.googleapis.com/v1/webhooks/<your_webhook_id>/rotate_secret" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GOOGLE_API_KEY" \
-d '{
"revocation_behavior": "REVOKE_PREVIOUS_SECRETS_AFTER_H24"
}'
Cómo controlar solicitudes de webhook en un servidor
Cuando se produzca un evento al que te suscribiste, tu URL de webhook recibirá una solicitud HTTP POST. Tu extremo debe responder con un código de estado 2xx en unos segundos para evitar un reintento. Para garantizar la entrega, la API de Gemini vuelve a intentar automáticamente las solicitudes fallidas durante 24 horas con una retirada exponencial.
Gemini sigue estrictamente la especificación de Webhooks Estándar para los encabezados de seguridad. Verifica la carga útil en tu servidor con las firmas de encabezado firmadas y tu secreto de firma estático almacenado. Consulta la sección Webhook envelope para obtener información sobre la carga útil.
Este es un ejemplo en el que se usa Flask para el objeto de escucha HTTP:
Python
# pip install flask standardwebhooks
import os
from flask import Flask, request, jsonify
# Standard verification wrapper for Standard Webhook Headers
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
SIGNING_SECRET = os.environ.get('WEBHOOK_SIGNING_SECRET')
@app.route('/gemini-callback', methods=['POST'])
def gemini_callback():
payload = request.get_data(as_text=True)
headers = request.headers
try:
wh = Webhook(SIGNING_SECRET)
event = wh.verify(payload, headers)
except WebhookVerificationError as e:
return jsonify({"error": "Signature invalid"}), 400
# Process thin payload contents
if event.get("type") in ("batch.completed", "video.generated"):
uri = event['data']['output_file_uri']
print(f"Batch finished! Results at: {uri}")
return jsonify({"status": "received"}), 200
if __name__ == "__main__":
app.run(port=8000)
JavaScript
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";
import express from "express";
const app = express();
const client = new GoogleGenAI({ webhookSecret: process.env.WEBHOOK_SIGNING_SECRET });
// Don't use express.json() because signature verification needs the raw text body
app.use(express.text({ type: "application/json" }));
app.post("/gemini-callback", async (req, res) => {
const payload = await req.text();
const headers: Record<string, string> = {};
req.headers.forEach((value, key) => {
headers[key] = value;
});
try {
const wh = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);
const event = wh.verify(payload, headers) as Record<string, any>;
// Process thin payload contents
if (event.type === "batch.completed" || event.type === "video.generated") {
const uri = event.data.output_file_uri;
console.log(`Job finished! Results at: ${uri}`);
}
res.status(200).json({ status: "received" });
} catch (e) {
console.error("Webhook verification failed:", e);
res.status(400).send("Invalid signature");
}
});
app.listen(8000, () => {
console.log("Webhook server is running on port 8000");
});
Webhooks dinámicos
Los webhooks dinámicos te permiten vincular un extremo de webhook a una configuración de solicitud específica, lo que resulta ideal para las colas de orquestación de agentes. Los webhooks dinámicos aprovechan las firmas JWKS de clave pública asimétrica en lugar de los secretos simétricos.
Envía una solicitud dinámica
Agrega un webhook_config cuando se active un trabajo asíncrono (p.ej., cuando se cree un lote).
Python
from google import genai
from google.genai import types
client = genai.Client()
file_batch_job = client.batches.create(
model="gemini-3-flash-preview",
src="files/uploaded_file_id",
config={
"display_name": "My Setup",
"webhook_config": {
"uris": ["https://my-api.com/gemini-webhook-dynamic"],
"user_metadata":{"job_group": "nightly-eval", "priority": "high"}
}
}
)
JavaScript
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI();
async function createBatchWithWebhook() {
const fileBatchJob = await client.batches.create({
model: "gemini-3-flash-preview",
src: "files/uploaded_file_id",
config: {
displayName: "My Setup",
webhookConfig: {
uris: ["https://my-api.com/gemini-webhook-dynamic"],
user_metadata: {"job_group": "nightly-eval", "priority": "high"}
},
},
});
}
REST
curl -X POST \
"https://generativelanguage.googleapis.com/v1/models/gemini-3-flash-preview:batchCreate" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: $GOOGLE_API_KEY" \
-d '{
"src": "files/uploaded_file_id",
"config": {
"display_name": "My Setup",
"webhook_config": {
"uris": ["https://my-api.com/gemini-webhook-dynamic"],
"user_metadata": {"job_group": "nightly-eval", "priority": "high"}
}
}
}'
Verifica firmas dinámicas (JWKS)
Las solicitudes de webhook dinámico emiten una firma de token web JSON (JWT). Tu objeto de escucha debe extraer la firma y verificarla con los endpoints de certificados públicos de Google.
Python
import jwt
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# Google public cert list endpoint
JWKS_URI = "https://generativelanguage.googleapis.com/.well-known/jwks.json"
def load_google_public_key(kid):
response = requests.get(JWKS_URI).json()
for key_item in response.get('keys', []):
if key_item.get('kid') == kid:
# Convert JWK to Cert wrapper
return jwt.algorithms.RSAAlgorithm.from_jwk(key_item)
return None
@app.route('/gemini-webhook-dynamic', methods=['POST'])
def dynamic_handler():
payload = request.get_data(as_text=True)
headers = request.headers
token = headers.get('Webhook-Signature')
if not token:
return jsonify({"error": "No signature header"}), 400
try:
# Extract kid from JWT header
unverified_headers = jwt.get_unverified_header(token)
pub_key = load_google_public_key(unverified_headers.get('kid'))
if not pub_key:
return jsonify({"error": "Key cert not found"}), 400
# Verify Signature against expected audience (e.g., your project client ID)
event = jwt.decode(
token,
pub_key,
algorithms=["RS256"],
audience="your-configured-audience"
)
except Exception as e:
return jsonify({"error": "Invalid Dynamic signature", "details": str(e)}), 400
print("Verified Dynamic payload success.")
return jsonify({"status": "received"}), 200
JavaScript
import { GoogleGenAI } from "@google/genai";
import express from "express";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const app = express();
app.use(express.text({ type: 'application/json' }));
const client = jwksClient({
jwksUri: "https://generativelanguage.googleapis.com/.well-known/jwks.json"
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
app.post('/gemini-webhook-dynamic', (req, res) => {
const token = req.headers['webhook-signature'];
if (!token) {
return res.status(400).json({ error: "No signature header" });
}
jwt.verify(
token,
getKey,
{
algorithms: ["RS256"],
audience: "your-configured-audience"
},
(err, decoded) => {
if (err) {
return res.status(400).json({ error: "Invalid Dynamic signature", details: err.message });
}
console.log("Verified Dynamic payload success.");
res.status(200).json({ status: "received" });
}
);
});
Sobre del webhook
Para evitar la congestión del ancho de banda, los webhooks de Gemini usan un modelo de carga útil delgada para entregar datos. Las entregas envían una instantánea que contiene detalles del estado y punteros a los resultados, en lugar del archivo de salida sin procesar.
A continuación, se muestra un ejemplo del formato de carga útil:
{
"type": "batch.completed",
"version": "v1",
"timestamp": "2026-01-22T12:00:00Z",
"data": {
"id": "batch_123456",
"output_file_uri": "gs://my-bucket/results.jsonl",
"error_count": 0
}
}
Referencia del catálogo de eventos
Los siguientes eventos se activan para los trabajos de asistencia:
| Tipo de evento | Activador | Elemento de carga útil (data) |
|---|---|---|
batch.succeeded |
El procesamiento finalizó correctamente. | id, output_file_uri |
batch.cancelled |
El usuario canceló la solicitud | id |
batch.expired |
El lote no se procesó (finalizó) en el plazo de 24 horas | id |
batch.failed |
El trabajo por lotes falló (error del sistema o de validación). | id, error_code, error_message |
interaction.requires_action |
Llamada a función, el usuario debe hacer algo | id |
interaction.completed |
La LRO en la API de Interactions se completó correctamente | id |
interaction.failed |
No se pudo realizar el LRO en la API de Interactions (error del sistema o de validación). | id, error_code, error_message |
interaction.cancelled |
Se canceló el LRO en la API de Interactions | id |
video.generated |
Se completó el LRO de generación de video. | file_id, video_uri |
Prácticas recomendadas
Para garantizar un funcionamiento confiable y escalable, haz lo siguiente:
- Verificación estricta de protección contra repeticiones: Todas las solicitudes incluyen un encabezado
webhook-timestamp. Siempre valida esta marca de tiempo en la capa de configuración del servidor para rechazar cargas útiles con una antigüedad superior a 5 minutos (para mitigar los ataques de reproducción). - Procesa de forma asíncrona: Responde con
2xx OKinmediatamente después de detectar una firma válida y pon en cola las operaciones de análisis de forma interna. Los tiempos de espera prolongados del receptor activarán un ciclo de reintento de entrega. - Control de la deduplicación: Los webhooks estándar realizan la entrega "al menos una vez". Usa el encabezado
webhook-idcoherente para controlar los posibles duplicados en flujos con mayor congestión.
Próximos pasos
- API de Batch: Utiliza webhooks para automatizar extremos de gran volumen.