Riferimento API Webhooks

Formato del payload, intestazioni, verifica della firma, tentativi e lista completa degli eventi.

Richiesta

Ogni consegna è un POST con un corpo JSON e queste intestazioni: Serpfull-Signature, Serpfull-Timestamp, Serpfull-Event-Id, Serpfull-Event-Type, Serpfull-Delivery-Attempt (1–8), Serpfull-Api-Version (2026-10-26) e User-Agent Serpfull-Webhooks/2.0.

Payload

Campi: id (id evento, lo stesso tra tentativi e reinvii), type, api_version, created_at (ISO 8601, UTC), created (secondi Unix), org_id, site (id, dominio e nome del sito web, o null per eventi dell'organizzazione) e data (specifico dell'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 è t=TIMESTAMP,v1=SIGNATURE. SIGNATURE è l'HMAC-SHA256 esadecimale della stringa TIMESTAMP + "." + corpo della richiesta raw, firmata con il segreto del tuo endpoint. Durante una rotazione del segreto ci sono due valori v1; accetta la richiesta se uno dei due corrisponde. Confronta in tempo costante e rifiuta timestamp a più di 5 minuti dal tuo orologio per bloccare i replay.

Verifica in 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()
})

Verifica in 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 "", 200

Risposte e ritentativi

  • 2xx entro 10 secondi: consegnato.
  • Qualsiasi altro caso, timeout o errore di connessione: ritentato dopo 1 m, 5 m, 30 m, 2 h, 6 h, 12 h e 24 h (8 tentativi in totale).
  • 410 Gone: smettiamo di ritentare quell'evento.
  • Le consegne avvengono almeno una volta e possono arrivare fuori ordine; deduplica sull'id evento e usa created_at per ordinare.

Eventi

  • serp.run.completed: Una ricerca è terminata e qualcuno ha cliccato il tuo annuncio.
  • serp.run.not_found: Una ricerca non ha trovato il tuo sito nei risultati.
  • serp.run.site_challenge: Il tuo sito ha mostrato una pagina di sfida durante una ricerca.
  • serp.run.failed: Una ricerca è fallita (timeout, bloccata o altro errore).
  • serp.term.auto_paused: Un termine è stato messo in pausa dopo 3 ricerche consecutive senza trovare il tuo sito.
  • serp.failure_rate.high: Oltre la metà delle ricerche di un sito è fallita in 24 ore.
  • local.run.completed: Una ricerca Local Business è terminata e qualcuno ha aperto il tuo annuncio.
  • local.run.not_found: Una ricerca Local Business non ha trovato il tuo annuncio.
  • local.run.site_challenge: Il tuo sito ha mostrato una pagina di sfida durante una ricerca Local Business.
  • local.run.failed: Una ricerca Local Business è fallita.
  • local.rank.changed: Una posizione locale monitorata sulla mappa si è spostata.
  • aio.run.completed: Una ricerca AI Overview è terminata e qualcuno ha cliccato la tua citazione.
  • aio.run.not_found: Una ricerca AI Overview non ha trovato il tuo sito citato.
  • aio.run.site_challenge: Il tuo sito ha mostrato una pagina di sfida durante una ricerca AI Overview.
  • aio.run.failed: Una ricerca AI Overview è fallita.
  • aio.citation.gained: Il tuo sito ha iniziato a essere citato nell'AI Overview per un termine monitorato.
  • aio.citation.lost: Il tuo sito ha smesso di essere citato nell'AI Overview per un termine monitorato.
  • serp.rank.changed: Una posizione monitorata si è spostata.
  • competitor.rank.changed: Un concorrente monitorato si è spostato di 3+ posizioni o è entrato/uscito dalla top 10 o 30.
  • rank_ai.summary_ready: Un report Rank AI è terminato, con punteggio di salute, riepilogo e raccomandazioni principali.
  • rank_ai.review_ready: Una revisione pagina Rank AI è terminata, con punteggio contenuto, modifiche suggerite e argomenti mancanti.
  • website.verified: La proprietà di un sito è stata verificata.
  • uptime.monitor.down: Un monitor di uptime ha confermato un'interruzione.
  • uptime.monitor.up: Un monitor di uptime si è ripristinato.
  • domains.expiry.warning: La registrazione di un dominio o un certificato SSL è vicino alla scadenza.
  • domains.change.detected: Un dominio monitorato è cambiato (DNS, registrar, certificato e altro).
  • ssl.certificate.expiring: Un certificato scade entro la finestra di avviso.
  • audit.completed: Un audit SEO è terminato.
  • audit.score_dropped: Il punteggio SEO di una pagina è sceso di 5 punti o più.
  • audit.issue.new: Un audit ha trovato problemi che prima non c'erano.
  • analytics.traffic.spike: Le visite sono aumentate bruscamente rispetto al livello abituale.
  • analytics.traffic.drop: Le visite sono diminuite drasticamente rispetto al livello abituale.
  • backlinks.domain.new: Sono stati trovati nuovi domini di riferimento.
  • backlinks.domain.lost: Sono stati persi domini di riferimento.
  • bot.attack.detected: Il traffico di bot dannosi è aumentato ben oltre il normale.
  • cookies.scan.completed: La scansione dei cookie è terminata.
  • report.generated: È stato generato un report.
  • exchange.installed: Il blocco Partners è stato trovato e il sito web è stato aggiunto.
  • exchange.link_missing: Il blocco Partners è scomparso, quindi i tuoi link sono in pausa.
  • exchange.partner_added: Un partner è stato aggiunto al tuo blocco, o tu al loro.
  • billing.subscription.updated: Un abbonamento è stato creato, modificato o annullato.
  • plan.limit.reached: Un sito web ha utilizzato tutta la quota del piano.
  • billing.payment.failed: Un pagamento per il tuo account non è andato a buon fine.
  • workflow.run.succeeded: Un'esecuzione live del workflow è terminata con successo.
  • workflow.run.failed: Un'esecuzione live del workflow è fallita.
  • workflow.approval.requested: Un workflow è in attesa che qualcuno approvi un passaggio.
  • workflow.custom: Un workflow ha inviato un evento personalizzato con il proprio payload.

Esegui eventi

serp.run.*, local.run.* e aio.run.* contengono run_id, website_id, search_term_id, module, outcome, position, page, dwell_seconds, pages_visited, device, country, started_at, finished_at e error_code. completed significa che la lista è stata trovata e cliccata, not_found che non era nei risultati, site_challenge che il tuo sito ha mostrato una pagina di sfida, e failed copre timeout, blocchi e altri errori.

Eventi di test

test.ping viene inviato solo quando premi Invia evento di test (o usi webhooks_send_test). I suoi dati hanno un messaggio e la tua organizzazione.

Aggiornato