Ricevere i webhook
Un webhook è l'alternativa al chiedere. Interrogare un mercato ogni cinque minuti per cogliere una variazione che avviene due volte al giorno è la fonte più comune di quota sprecata, e una consegna non costa quota affatto.
Creare un endpoint
curl -sS -X POST "https://api.skautik.com/v1/api/webhooks" \
-H "Authorization: Bearer $SKAUTIK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/skautik",
"events": ["property.created", "property.updated", "property.withdrawn"]
}'La risposta porta il segreto di firma una sola volta:
{
"data": {
"webhook": { "id": "whk_1b7e6c19a1", "url": "…", "active": true },
"secret": "whsec_uEw7ugUA3saVyNo087Sg3XvhBnWI0txICtik-ySKD2k"
}
}Conservalo prima di chiudere la risposta. Non viene mai più mostrato, e l'unico rimedio è ruotarlo, il che invalida quello vecchio.
Eventi
| Evento | Quando |
|---|---|
property.created | Un immobile è stato pubblicato, da te, da un'importazione o da un'agenzia. |
property.updated | È cambiato un campo qualsiasi, compreso un cambio di prezzo. |
property.withdrawn | Un immobile ha lasciato il mercato. Non è una cancellazione: la scheda resta leggibile. |
inquiry.created | Qualcuno ha chiesto informazioni su uno dei tuoi immobili. |
Iscriviti a ciò su cui interverrai. Un endpoint iscritto a tutto che
filtra nel codice paga comunque il costo di ricevere tutto, e un'importazione molto
attiva rende property.updated l'evento più rumoroso della piattaforma.
Verificare la firma
Fallo prima di leggere il corpo. Un endpoint webhook non verificato è un'API pubblica che scrive nel tuo database su richiesta di chiunque indovini l'URL.
Ogni consegna porta tre intestazioni:
Skautik-Signature: t=1786680348,v1=8f2a41c9d0b7e6c19a139404cb173d23fcb3331c4e…
Skautik-Event: property.updated
Skautik-Delivery: dlv_9c4e1f2308La firma è HMAC-SHA256(secret, timestamp + "." + body), in esadecimale. La
marca temporale sta dentro la firma e non solo accanto, ed è la parte
che conta: se venisse firmato solo il corpo, una consegna intercettata potrebbe essere ripetuta
sul tuo endpoint per sempre e la firma corrisponderebbe comunque.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((piece) => piece.split("=") as [string, string]),
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) {
return false;
}
// Rifiuta qualsiasi cosa più vecchia di cinque minuti. Senza questo la firma
// vale per sempre e una richiesta intercettata non smette mai di funzionare.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) {
return false;
}
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// A tempo costante. Un === normale rivela la posizione del primo byte
// sbagliato, e tanto basta per falsificare una firma con abbastanza tentativi.
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}import hashlib
import hmac
import time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(piece.split("=", 1) for piece in header.split(","))
timestamp = parts.get("t")
signature = parts.get("v1")
if not timestamp or not signature:
return False
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)Firma i byte grezzi, non un oggetto riserializzato. Analizzare il JSON e riscriverlo cambia l'ordine delle chiavi e gli spazi, e la firma non corrisponderà mai. Alla maggior parte dei framework va detto esplicitamente di conservare il corpo grezzo.
Rispondere
Rispondi 2xx appena hai verificato la firma e scritto il contenuto
da qualche parte in modo duraturo. Fai il lavoro vero dopo, su una coda.
Un endpoint che geocodifica un indirizzo, scrive su tre tabelle e manda un'e-mail prima di rispondere è un endpoint che va in timeout sotto carico, il che trasforma una consegna in sei quando riproviamo, il che peggiora il carico.
Qualsiasi cosa diversa da un 2xx è un fallimento e verrà riprovata.
Nuovi tentativi
Una consegna fallita viene riprovata fino a sei tentativi con attesa esponenziale.
next_retry_at sulla scheda di consegna dice quando è previsto il successivo.
Le consegne sono almeno una volta, non esattamente una volta. Un timeout dalla tua parte dopo
che avevi confermato conta comunque come fallimento per noi, e rivedrai l'evento.
Rendi idempotente la gestione: usa come chiave Skautik-Delivery, oppure l'id della scheda e il suo
updated_at, e tratta una ripetizione come un'operazione a vuoto.
Nemmeno l'ordine è garantito. Due aggiornamenti di uno stesso immobile possono arrivare al
contrario, quindi confronta updated_at prima di sovrascrivere invece di fidarti
dell'ordine di arrivo.
Quando qualcosa non va
GET /v1/api/webhooks/{webhook_id}/deliveries elenca cosa abbiamo tentato, con il
codice di stato che hai restituito e quanti tentativi sono serviti. È il primo posto in cui
guardare quando i dati hanno smesso di arrivare, e di solito risponde alla domanda prima che
tu apra i tuoi log.
POST /v1/api/webhooks/{webhook_id}/test invia una consegna sintetica, il
modo rapido per controllare che un nuovo endpoint sia raggiungibile e verifichi correttamente
prima che qualcosa ne dipenda.
Una lista di controllo
- Verifica la firma prima di leggere il corpo.
- Rifiuta le consegne più vecchie di qualche minuto.
- Confronta le firme a tempo costante.
- Rispondi in fretta; lavora dopo.
- Gestisci ripetizioni e arrivi fuori ordine.
- Iscriviti solo agli eventi su cui intervieni.
- Tieni il segreto fuori dal tuo repository, come qualsiasi altra credenziale.