Referencia de la API de webhooks
Formato de la carga, cabeceras, verificación de la firma, reintentos y la lista completa de eventos.
Petición
Cada entrega es un POST con un cuerpo JSON y estas cabeceras: Serpfull-Signature, Serpfull-Timestamp, Serpfull-Event-Id, Serpfull-Event-Type, Serpfull-Delivery-Attempt (1–8), Serpfull-Api-Version (2026-10-26) y User-Agent Serpfull-Webhooks/2.0.
Carga
Campos: id (id del evento, el mismo en reintentos y reenvíos), type, api_version, created_at (ISO 8601, UTC), created (segundos Unix), org_id, site (id, dominio y nombre del sitio web, o null en eventos de organización) y data (específico de cada evento).
{
"id": "6f1c2b8e-3a4d-4e5f-9a1b-2c3d4e5f6a7b",
"type": "uptime.monitor.down",
"api_version": "2026-10-26",
"created_at": "2026-10-26T09:14:03.120Z",
"created": 1793006043,
"org_id": "0d9a…",
"site": { "id": "8b2e…", "domain": "example.co.uk", "name": "Example" },
"data": { "monitor_id": "…", "url": "https://example.co.uk/", "reason": "timeout" }
}Firma
Serpfull-Signature es t=TIMESTAMP,v1=SIGNATURE. SIGNATURE es el HMAC-SHA256 en hexadecimal de la cadena TIMESTAMP + "." + el cuerpo original de la petición, con el secreto de firma de tu endpoint como clave. Durante una rotación del secreto hay dos valores v1; acepta la petición si coincide cualquiera de ellos. Compara en tiempo constante y rechaza marcas de tiempo con más de 5 minutos de diferencia respecto a tu reloj para bloquear reenvíos maliciosos.
Verificar en Node.js
import crypto from "node:crypto"
// Use the raw request body (a string or Buffer), not re-serialised JSON.
export function verifySerpfull(rawBody, header, secret, toleranceSeconds = 300) {
const parts = header.split(",").map((p) => p.trim().split("="))
const t = Number(parts.find(([k]) => k === "t")?.[1])
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest()
return parts.some(([k, v]) => k === "v1" && v?.length === expected.length * 2 &&
crypto.timingSafeEqual(Buffer.from(v, "hex"), expected))
}
// Express example
app.post("/webhooks/serpfull", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySerpfull(req.body.toString("utf8"), req.get("serpfull-signature") ?? "", process.env.SERPFULL_WEBHOOK_SECRET)) {
return res.status(400).end()
}
const event = JSON.parse(req.body)
// Deduplicate on event.id (also sent as the Serpfull-Event-Id header), then handle event.type.
res.status(200).end()
})Verificar en Python
import hashlib, hmac, time
def verify_serpfull(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
try:
t = int(next(v for k, v in parts if k == "t"))
except (StopIteration, ValueError):
return False
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(k == "v1" and hmac.compare_digest(v, expected) for k, v in parts)
# Flask example
@app.post("/webhooks/serpfull")
def serpfull_webhook():
if not verify_serpfull(request.get_data(), request.headers.get("Serpfull-Signature", ""), os.environ["SERPFULL_WEBHOOK_SECRET"]):
abort(400)
event = request.get_json()
# Deduplicate on event["id"], then handle event["type"].
return "", 200Respuestas y reintentos
- 2xx en menos de 10 segundos: entregado.
- Cualquier otra respuesta, un tiempo de espera agotado o un error de conexión: se reintenta tras 1 m, 5 m, 30 m, 2 h, 6 h, 12 h y 24 h (8 intentos en total).
- 410 Gone: dejamos de reintentar ese evento.
- Las entregas son al menos una vez y pueden llegar desordenadas; elimina duplicados con el id del evento y usa created_at para ordenarlas.
Eventos
- serp.run.completed: Una ejecución de búsqueda terminó e hizo clic en tu ficha.
- serp.run.not_found: Una ejecución de búsqueda no encontró tu sitio web en los resultados.
- serp.run.site_challenge: Tu sitio web mostró una página de verificación durante una ejecución.
- serp.run.failed: Una ejecución de búsqueda falló (tiempo agotado, bloqueo u otro error).
- serp.term.auto_paused: Se pausó un término después de 3 ejecuciones seguidas sin encontrar tu sitio web.
- serp.failure_rate.high: Más de la mitad de las ejecuciones de un sitio web fallaron en 24 horas.
- local.run.completed: Una ejecución de Negocio local terminó y abrió tu ficha.
- local.run.not_found: Una ejecución de Negocio local no encontró tu ficha.
- local.run.site_challenge: Tu sitio web mostró una página de verificación durante una ejecución de Negocio local.
- local.run.failed: Una ejecución de Negocio local falló.
- aio.run.completed: Una ejecución de AI Overview terminó e hizo clic en tu cita.
- aio.run.not_found: Una ejecución de AI Overview no encontró tu sitio web citado.
- aio.run.site_challenge: Tu sitio web mostró una página de verificación durante una ejecución de AI Overview.
- aio.run.failed: Una ejecución de AI Overview falló.
- serp.rank.changed: Una posición seguida cambió.
- competitor.rank.changed: Un competidor seguido se movió 3 o más puestos, o entró o salió del top 10 o del top 30.
- rank_ai.summary_ready: Un informe de Rank AI terminó, con la puntuación de salud, el resumen y las principales recomendaciones.
- rank_ai.review_ready: Ha terminado una revisión de página de Rank AI, con la puntuación de contenido, los cambios sugeridos y los temas que faltan.
- website.verified: Se verificó la propiedad de un sitio web.
- uptime.monitor.down: Un monitor de disponibilidad confirmó una caída.
- uptime.monitor.up: Un monitor de disponibilidad se recuperó.
- domains.expiry.warning: El registro de un dominio o un certificado SSL está a punto de caducar.
- domains.change.detected: Un dominio vigilado cambió (DNS, registrador, certificado y más).
- audit.completed: Terminó una auditoría SEO.
- audit.score_dropped: La puntuación SEO de una página bajó 5 puntos o más.
- backlinks.domain.new: Se encontraron nuevos dominios de referencia.
- backlinks.domain.lost: Se perdieron dominios de referencia.
- bot.attack.detected: El tráfico de bots maliciosos subió muy por encima de lo normal.
- cookies.scan.completed: Terminó un análisis de cookies.
- report.generated: Se generó un informe.
- exchange.installed: Se encontró tu bloque de Partners y el sitio web se unió.
- exchange.link_missing: Tu bloque de Partners desapareció, así que tus enlaces están en pausa.
- exchange.partner_added: Se añadió un partner a tu bloque, o tú al suyo.
- billing.subscription.updated: Se creó, cambió o canceló una suscripción.
- plan.limit.reached: Un sitio web agotó una asignación de su plan.
Eventos de ejecución
serp.run.*, local.run.* y aio.run.* incluyen run_id, website_id, search_term_id, module, outcome, position, page, dwell_seconds, pages_visited, device, country, started_at, finished_at y error_code. completed significa que se encontró la ficha y se hizo clic, not_found que no estaba en los resultados, site_challenge que tu sitio web mostró una página de verificación, y failed cubre tiempos de espera agotados, bloqueos y otros errores.
Eventos de prueba
test.ping solo se envía cuando pulsas Enviar evento de prueba (o usas webhooks_send_test). Su data incluye un mensaje y tu organización.
Updated