Webhooks empfangen
Ein Webhook ist die Alternative zum Fragen. Einen Markt alle fünf Minuten abzufragen, um eine Änderung zu erwischen, die zweimal täglich passiert, ist die häufigste Quelle verschwendeten Kontingents, und eine Zustellung kostet gar kein Kontingent.
Einen Endpunkt anlegen
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"]
}'Die Antwort trägt das Signaturgeheimnis einmal:
{
"data": {
"webhook": { "id": "whk_1b7e6c19a1", "url": "…", "active": true },
"secret": "whsec_uEw7ugUA3saVyNo087Sg3XvhBnWI0txICtik-ySKD2k"
}
}Speichern Sie es, bevor Sie die Antwort schließen. Es wird nie wieder gezeigt, und der einzige Ausweg ist, es zu wechseln, was das alte ungültig macht.
Ereignisse
| Ereignis | Wann |
|---|---|
property.created | Ein Objekt wurde veröffentlicht, von Ihnen, von einem Import oder von einem Makler. |
property.updated | Irgendein Feld hat sich geändert, einschließlich einer Preisänderung. |
property.withdrawn | Ein Objekt hat den Markt verlassen. Keine Löschung: Der Datensatz bleibt lesbar. |
inquiry.created | Jemand hat zu einem Ihrer Objekte angefragt. |
Abonnieren Sie, worauf Sie handeln werden. Ein Endpunkt, der alles abonniert und
im Code filtert, zahlt trotzdem den Preis dafür, alles zu empfangen, und ein
reger Import macht property.updated zum lautesten Ereignis der Plattform.
Die Signatur prüfen
Tun Sie das, bevor Sie den Rumpf lesen. Ein ungeprüfter Webhook-Endpunkt ist eine öffentliche API, die auf Zuruf jedes Menschen, der die URL errät, in Ihre Datenbank schreibt.
Jede Zustellung trägt drei Header:
Skautik-Signature: t=1786680348,v1=8f2a41c9d0b7e6c19a139404cb173d23fcb3331c4e…
Skautik-Event: property.updated
Skautik-Delivery: dlv_9c4e1f2308Die Signatur ist HMAC-SHA256(secret, timestamp + "." + body), hexkodiert. Der
Zeitstempel steckt in der Signatur und nicht nur daneben, und genau darauf kommt
es an: Wäre nur der Rumpf signiert, ließe sich eine mitgeschnittene Zustellung
ewig an Ihrem Endpunkt wiederholen, und die Signatur würde weiter passen.
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;
}
// Alles über fünf Minuten ablehnen. Ohne das ist die Signatur ewig gültig
// und eine mitgeschnittene Anfrage funktioniert für immer.
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");
// In konstanter Zeit. Ein schlichtes === verrät die Position des ersten
// falschen Bytes, und das genügt, um mit genug Versuchen eine Signatur zu
// fälschen.
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)Signieren Sie die rohen Bytes, kein erneut serialisiertes Objekt. JSON zu lesen und wieder auszugeben ändert Schlüsselreihenfolge und Leerzeichen, und die Signatur wird nie passen. Den meisten Frameworks muss man ausdrücklich sagen, dass sie den rohen Rumpf behalten sollen.
Antworten
Antworten Sie mit 2xx, sobald Sie die Signatur geprüft und die Nutzlast
dauerhaft abgelegt haben. Die eigentliche Arbeit erledigen Sie danach, über eine
Warteschlange.
Ein Endpunkt, der eine Adresse geokodiert, in drei Tabellen schreibt und eine E-Mail sendet, bevor er antwortet, ist ein Endpunkt, der unter Last in ein Timeout läuft, was aus einer Zustellung sechs macht, weil wir es erneut versuchen, was die Last weiter erhöht.
Alles außer 2xx gilt als Fehlschlag und wird wiederholt.
Wiederholungen
Eine fehlgeschlagene Zustellung wird bis zu sechsmal mit wachsender Wartezeit
wiederholt. next_retry_at im Zustellungsdatensatz sagt, wann die nächste fällig
ist.
Zustellungen erfolgen mindestens einmal, nicht genau einmal. Ein Timeout auf
Ihrer Seite nach dem Festschreiben gilt für uns trotzdem als Fehlschlag, und Sie
sehen das Ereignis erneut. Machen Sie die Verarbeitung idempotent: über
Skautik-Delivery oder über die Datensatz-id und deren updated_at, und
behandeln Sie eine Wiederholung als Nichtstun.
Auch die Reihenfolge ist nicht zugesichert. Zwei Änderungen an einem Objekt
können verkehrt herum ankommen, vergleichen Sie also updated_at vor dem
Überschreiben, statt der Ankunftsreihenfolge zu trauen.
Wenn etwas nicht stimmt
GET /v1/api/webhooks/{webhook_id}/deliveries listet, was wir versucht haben,
mit dem Statuscode, den Sie zurückgegeben haben, und der Zahl der Versuche. Das
ist die erste Anlaufstelle, wenn keine Daten mehr ankommen, und beantwortet die
Frage meist, bevor Sie Ihre eigenen Protokolle öffnen.
POST /v1/api/webhooks/{webhook_id}/test sendet eine künstliche Zustellung, was
der schnelle Weg ist, einen neuen Endpunkt auf Erreichbarkeit und richtige
Prüfung zu testen, bevor Sie sich darauf verlassen.
Eine Prüfliste
- Signatur prüfen, bevor Sie den Rumpf lesen.
- Zustellungen ablehnen, die älter als ein paar Minuten sind.
- Signaturen in konstanter Zeit vergleichen.
- Schnell antworten, danach arbeiten.
- Wiederholungen und vertauschte Reihenfolge behandeln.
- Nur Ereignisse abonnieren, auf die Sie handeln.
- Das Geheimnis aus dem Repository heraushalten, wie jedes andere Zugangsdatum.