Ir al contenido

Integraciones

Webhooks

Enviamos un POST a tu servidor cuando termina una recopilación, para que tu código no tenga que estar preguntando si ya acabó.

Qué planes los incluyen

Los webhooks vienen en los planes Starter, Pro y Scale. En el plan Free, toda llamada de webhooks responde 402 PLAN_GATE y no enviamos nada. details.upgradeTo indica a qué plan cambiar: starter. Cambia de plan en la consola.

Sin webhooks, tu código puede leer la recopilación con GET /v1/collections/{id} hasta que termine, o seguir su progreso en vivo.

Agrega un endpoint

Un endpoint es la dirección de tu servidor a la que enviamos el POST. Agrega uno con esta llamada, o en la página Webhooks de la consola:

curl -X POST https://api.gmaps.dev/v1/webhooks \
  -H "Authorization: Bearer $GMAPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/webhooks/gmaps" }'

La respuesta incluye secret, el secreto de firma, que empieza con whsec_. Es la única vez que lo mostramos. Guárdalo junto con tus otros secretos: con él, tu servidor comprueba que una solicitud vino de nosotros.

Hay dos campos más, ambos opcionales. events elige qué eventos enviar; si lo omites, se envían los dos. description es una nota para ti, por ejemplo cuál de tus servicios es este.

Qué necesita la URL

Revisamos la URL cuando la agregas, y otra vez antes de cada envío, porque la dirección detrás de un nombre puede cambiar:

  • Empieza con https://. Nunca enviamos tus resultados por una conexión sin cifrar.
  • Su nombre apunta a una dirección pública. Las redes privadas y tu propia máquina, como localhost, se rechazan. Para probar desde tu computadora, usa una herramienta que le dé una dirección https pública.
  • No lleva usuario ni contraseña. Lo que prueba que una solicitud vino de nosotros es la firma.
  • Todavía no es uno de tus endpoints. Si agregas la misma URL dos veces, la respuesta es 409 CONFLICT.
  • Es la dirección final. No seguimos redirecciones.

Eventos

EventoCuándo lo enviamos
collection.completedUna recopilación terminó con estado succeeded o partial. Recopilaciones explica los dos. Sus conteos son cero cuando la búsqueda no encontró nada.
collection.failedUna recopilación terminó sin ningún lugar, porque sus búsquedas no pudieron ejecutarse. No costó nada. data.error dice por qué y qué hacer.
pingSolo cuando envías una prueba.

Una recopilación que cancelas no envía nada: ya sabes que se detuvo.

Qué enviamos

Un cuerpo JSON pequeño, con conteos y un enlace, nunca los lugares en sí. Un collection.completed se ve así:

{
  "id": "3f6c2a9e-1b7d-4e58-8a21-6d0f4b9c2e75",
  "timestamp": "2026-09-24T14:05:12.345Z",
  "type": "collection.completed",
  "data": {
    "collection": {
      "id": "8c1f5e2a-7b3d-4f60-9e14-2a6d0b9c7f31",
      "status": "succeeded",
      "stopReason": null
    },
    "counts": {
      "placesDelivered": 212,
      "placesFresh": 180,
      "placesCached": 32,
      "creditsCharged": 180,
      "creditsRefunded": 0
    },
    "resultUrl": "https://api.gmaps.dev/v1/collections/8c1f5e2a-7b3d-4f60-9e14-2a6d0b9c7f31/places"
  }
}
  • id: el id del propio evento. Es el mismo en cada reintento.
  • type y timestamp: qué evento es y cuándo ocurrió.
  • data.collection: el id y el status de la recopilación, y stopReason, que dice por qué se detuvo antes o no encontró nada. Es null cuando la recopilación buscó toda la zona o llegó a su maxPlaces. Recopilaciones explica cada motivo.
  • data.counts: los lugares que entregó la recopilación y los créditos que cobró, igual que en la propia recopilación. Recopilaciones explica cada conteo.
  • data.resultUrl: dónde leer los lugares, una página a la vez, con tu clave de API.
  • Un collection.failed trae data.error en lugar de los conteos y el enlace, con los mismos code, message y messageKey que cualquier error de la API.

Cada solicitud también lleva estos encabezados:

EncabezadoQué contiene
content-typeapplication/json
x-gmaps-signatureCuándo la enviamos, y la firma. Mira más abajo.
x-gmaps-deliveryEl id del evento otra vez, para que detectes uno repetido antes de leer el cuerpo.
user-agentgmaps.dev webhooks

Verifica la firma

Cualquiera que encuentre tu URL puede enviarle una solicitud. La firma prueba que una solicitud vino de nosotros. Va en el encabezado x-gmaps-signature:

x-gmaps-signature: t=1790000000,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
  • t es cuándo la enviamos, en segundos Unix.
  • v1 es un HMAC-SHA256 de t, un punto y el cuerpo sin procesar. Se calcula con tu secreto de firma completo, incluido whsec_, y se escribe en hexadecimal. Un HMAC es un código que solo puede generar quien tiene el secreto.

Para verificarla, calcula el mismo HMAC y compara los dos. Rechaza la solicitud si son distintos, o si t está a más de 5 minutos de tu reloj, para que nadie pueda reenviar una solicitud antigua.

Usa el cuerpo sin procesar: los bytes exactos que recibiste. Si analizas el JSON y lo vuelves a convertir en texto, los bytes cambian y la verificación falla.

verifyWebhook del SDK hace todo esto:

import { verifyWebhook } from "gmaps-sdk";

const body = await request.text();
const signed = await verifyWebhook(
  process.env.GMAPS_WEBHOOK_SECRET,
  body,
  request.headers.get("x-gmaps-signature"),
);
if (!signed) return new Response(null, { status: 400 });
const event = JSON.parse(body);

Sin el SDK, son unas pocas líneas en cualquier lenguaje. En Python se ve así:

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str | None) -> bool:
    if not header:
        return False
    parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        sent = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - sent) > 300:
        return False
    signed = f"{sent}.".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Respuestas y reintentos

Responde con cualquier estado 2xx en cuanto llegue la solicitud, y deja el trabajo lento para después. Cualquier otra respuesta cuenta como una falla:

  • Un 5xx, 408, 425, 429, un tiempo de espera agotado o una conexión fallida: lo intentamos de nuevo, hasta 5 intentos en total. La espera habitual empieza en 30 segundos y se duplica después de cada intento fallido. Si el encabezado Retry-After pide más tiempo, usamos ese plazo para ese intento, con un tope de una hora.
  • Cualquier otro 4xx: no lo intentamos de nuevo. Tu servidor dijo que no, y volvería a decir que no.
  • Una redirección: no la seguimos y no lo intentamos de nuevo. Lo mismo pasa con un dominio que no apunta a ninguna dirección, o que apunta a una red privada. Apunta el endpoint a la dirección final y pública de tu servidor.

Por los reintentos, el mismo evento puede llegar más de una vez. Guarda los id que ya procesaste y omite los repetidos.

Cuándo dejamos de enviar

Si fallan 10 entregas seguidas, desactivamos el endpoint. Cada entrega cuenta una vez, cuando dejamos de reintentarla. Lo puedes ver en dos lugares:

  • La página Webhooks de la consola muestra el endpoint como desactivado y dice por qué.
  • GET /v1/webhooks responde con enabled: false, y disabledReason dice por qué, en inglés, con el último error. disabledReasonKey es el mismo motivo como clave de mensaje, y disabledReasonParams trae los valores que completan la frase. Así, un programa puede mostrar el motivo en el idioma de quien lo lee.

Mientras está desactivado, los eventos que se pierde no se guardan, así que nunca llegan. Para ponerte al día, lista tus recopilaciones con GET /v1/collections.

Arregla tu servidor y vuelve a activar el endpoint. Su conteo de fallas empieza de nuevo desde cero:

curl -X POST https://api.gmaps.dev/v1/webhooks/4d7a1c3e-9b2f-4e8a-b6d1-0c5f7e2a9b34 \
  -H "Authorization: Bearer $GMAPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'

El conteo también vuelve a cero cada vez que una entrega funciona.

Envía una prueba

Antes de que una recopilación real dependa de esto, envía un evento ping. Muestra si tu código lee la solicitud y si la firma coincide:

curl -X POST https://api.gmaps.dev/v1/webhooks/4d7a1c3e-9b2f-4e8a-b6d1-0c5f7e2a9b34/test \
  -H "Authorization: Bearer $GMAPS_API_KEY"
  • Responde al instante con la entrega, casi siempre todavía pending. La prueba se firma y se reintenta igual que un evento real.
  • Envía message en el cuerpo para poner tu propio texto en el evento y encontrarlo fácil en tus logs.
  • Un endpoint desactivado no se puede probar: la llamada responde 409 CONFLICT. Actívalo primero.

Mira qué enviamos

El historial de entregas lista lo que enviamos a un endpoint, de lo más reciente a lo más antiguo. Míralo cuando un evento no llegue:

curl https://api.gmaps.dev/v1/webhooks/4d7a1c3e-9b2f-4e8a-b6d1-0c5f7e2a9b34/deliveries \
  -H "Authorization: Bearer $GMAPS_API_KEY"
  • status: pending mientras seguimos intentando, luego delivered o failed.
  • attempts: cuántos intentos lleva.
  • responseStatus: el estado con que respondió tu servidor, o null si nunca respondió.
  • error: qué salió mal y cómo arreglarlo, en inglés. errorKey es la misma frase como clave de mensaje, y errorParams trae los valores que la completan. Así, un programa puede mostrarla en el idioma de quien lee.

Guardamos el historial 7 días. limit define cuántas entregas devuelve.

Cambia o elimina un endpoint

POST /v1/webhooks/{id} cambia events, description o enabled. Envía solo lo que quieras cambiar: lo demás queda como estaba.

  • Para pausar un endpoint, pon enabled en false. Conserva su secreto, así que tu código no cambia cuando lo vuelvas a activar.
  • DELETE /v1/webhooks/{id} elimina el endpoint y su historial de entregas. No se puede deshacer.
  • La URL y el secreto no se pueden cambiar. Para tener otros, elimina el endpoint y agrégalo de nuevo.