Webhooks
Den Change-Feed an Ihren Server geschickt bekommen, sobald etwas passiert: signiert, in der richtigen Reihenfolge, mit Wiederholungen. Mit Code zur Prüfung der Signatur.
Ein Webhook schickt die Änderungen Ihrer Datasets an eine URL auf Ihrem Server, sobald sie passieren: dieselben Änderungen, in derselben Reihenfolge und mit denselben Objekten wie der Change-Feed, ohne dass Sie abfragen müssen. Webhooks gibt es ab dem Plan Pro (oder als Zusatzprodukt).
Endpunkt einrichten
Öffnen Sie im Portal API → Webhooks und tragen Sie die URL Ihres Endpunkts ein (https://…). Sie können ihn auf
einige Ihrer Datasets und auf einige Arten von Änderungen beschränken (etwa nur REMOVED). Pricana zeigt das
Signing-Secret des Endpunkts (prc_whsec_…) einmal an: Bewahren Sie es bei Ihrem Code auf, wie einen API-Key.
Send test schickt sofort ein ping-Ereignis und zeigt die Antwort Ihres Servers. Ein neuer Endpunkt bekommt die
Änderungen ab dem Moment, in dem er eingerichtet wurde.
Was Ihr Endpunkt bekommt
Einen POST mit einem JSON-Body, bis zu 100 Änderungen auf einmal:
{
"id": "evt_5f1c0b2a9d7e4c3b8a6f0e1d",
"type": "changes",
"createdAt": "2026-10-07T09:12:31Z",
"changes": [
{
"id": 812,
"kind": "CHANGED",
"observedAt": "2026-10-06T21:03:00Z",
"price": 1399.0,
"previousPrice": 1499.0,
"changes": { "price": [1499.0, 1399.0] },
"item": { "id": 77, "title": "Lenovo ThinkPad X1 Carbon Gen 12", "url": "https://…", "currency": "EUR" }
}
],
"cursor": 812
}und diese Header:
| Header | |
|---|---|
Pricana-Signature | t=<unix seconds>,v1=<signature> (unten) |
Pricana-Event-Id | die id des Ereignisses: bei jedem Versuch dieselbe |
Pricana-Event-Type | changes, oder ping für einen Test |
Pricana-Delivery-Attempt | 1 beim ersten Versuch, dann 2, 3 … |
User-Agent | PricanaWebhooks/1.0 (+https://pricana.io/docs/webhooks) |
Ein ping enthält keine Änderungen, und cursor ist null. cursor ist die id der letzten Änderung im Ereignis: Müssen
Sie einmal per Abfrage aufholen, macht /changes?after=<cursor> dort weiter.
Schnell antworten
Antworten Sie innerhalb von 10 Sekunden mit einem beliebigen 2xx-Status. Erledigen Sie die eigentliche Arbeit nach
der Antwort (legen Sie das Ereignis zum Beispiel in eine Warteschlange): Eine langsame Antwort zählt als Fehlschlag. Der
Body Ihrer Antwort spielt keine Rolle; Pricana speichert seine ersten 300 Zeichen im Zustell-Log, damit Sie Fehler leichter
finden.
Signatur prüfen
Jeder kann eine Anfrage an Ihre URL schicken. Prüfen Sie deshalb, bevor Sie einen Body verwenden, dass er von Pricana
kommt: Der Header Pricana-Signature enthält einen Zeitstempel t und die Signatur v1, einen HMAC-SHA256 über t,
einen Punkt und den unveränderten Body, mit dem Secret Ihres Endpunkts als Schlüssel (dem ganzen String
prc_whsec_…), hexadezimal.
- Lesen Sie
tund jedesv1aus dem Header (nachdem ein Secret ersetzt wurde, kann es zweiv1geben). - Berechnen Sie
HMAC-SHA256(secret, t + "." + body)über den Body genau so, wie er angekommen ist, bevor Sie ihn parsen. - Akzeptieren Sie ihn, wenn das Ergebnis einem der
v1-Werte entspricht (in konstanter Zeit verglichen) undthöchstens 5 Minuten alt ist.
Python
import hashlib, hmac, os, time
def verify(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), None)
signatures = [v.encode() for k, v in parts if k == "v1"]
# isascii: "²".isdigit() ist wahr, int("²") schlägt fehl
if t is None or not (t.isascii() and t.isdigit()) or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest().encode()
# bytes statt str: compare_digest lehnt str mit Nicht-ASCII-Zeichen ab
return any(hmac.compare_digest(expected, s) for s in signatures)
# Flask
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["PRICANA_WEBHOOK_SECRET"]
@app.post("/pricana")
def pricana():
if not verify(request.get_data(), request.headers.get("Pricana-Signature", ""), SECRET):
abort(400)
event = request.get_json()
for change in event["changes"]:
... # queue it; skip change ids you already have
return "", 204Node.js
import crypto from 'node:crypto'
import express from 'express'
export function verify(body, header, secret, tolerance = 300) {
const parts = header.split(',').map((p) => p.split('='))
const t = parts.find(([k]) => k === 't')?.[1]
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v)
if (!t || !/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > tolerance) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(body).digest('hex')
const want = Buffer.from(expected)
// gleich viele Bytes, nicht nur Zeichen: sonst wirft timingSafeEqual
return signatures.some((s) => {
const got = Buffer.from(s)
return got.length === want.length && crypto.timingSafeEqual(got, want)
})
}
const app = express()
// the raw body: parsing JSON first would change the bytes that were signed
app.post('/pricana', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('Pricana-Signature') ?? '', process.env.PRICANA_WEBHOOK_SECRET)) {
return res.sendStatus(400)
}
const event = JSON.parse(req.body.toString('utf8'))
for (const change of event.changes) {
// queue it; skip change ids you already have
}
res.sendStatus(204)
})
app.listen(3000)PHP
<?php
function pricana_verify(string $body, string $header, string $secret, int $tolerance = 300): bool
{
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', $part, 2), 2, '');
if ($k === 't') $t = $v;
if ($k === 'v1') $signatures[] = $v;
}
if ($t === null || !ctype_digit($t) || abs(time() - (int) $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
foreach ($signatures as $s) {
if (hash_equals($expected, $s)) return true;
}
return false;
}
$body = file_get_contents('php://input');
if (!pricana_verify($body, $_SERVER['HTTP_PRICANA_SIGNATURE'] ?? '', getenv('PRICANA_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
foreach ($event['changes'] as $change) {
// queue it; skip change ids you already have
}
http_response_code(204);Java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class PricanaSignature {
/** body: the request body exactly as received, as UTF-8 text. */
public static boolean verify(String body, String header, String secret) throws Exception {
String t = null;
List<String> signatures = new ArrayList<>();
for (String part : header.split(",")) {
String[] kv = part.split("=", 2);
if (kv.length == 2 && kv[0].equals("t")) t = kv[1];
if (kv.length == 2 && kv[0].equals("v1")) signatures.add(kv[1]);
}
if (t == null || !t.matches("\\d+") || Math.abs(Instant.now().getEpochSecond() - Long.parseLong(t)) > 300) {
return false;
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] expected = HexFormat.of()
.formatHex(mac.doFinal((t + "." + body).getBytes(StandardCharsets.UTF_8)))
.getBytes(StandardCharsets.UTF_8);
for (String s : signatures) {
if (MessageDigest.isEqual(expected, s.getBytes(StandardCharsets.UTF_8))) return true;
}
return false;
}
}Reihenfolge, Wiederholungen und doppelte Zustellungen
- Der Reihe nach, eins nach dem anderen: Das nächste Ereignis geht erst hinaus, wenn Ihr Endpunkt das vorige angenommen hat. Innerhalb eines Ereignisses sind die Änderungen nach id sortiert.
- Wiederholungen: Antwortet Ihr Endpunkt nicht mit
2xx, versucht Pricana dasselbe Ereignis nach 1, 5, 15 und 30 Minuten erneut, danach nach 1 und 2 Stunden und dann alle 4 Stunden. Spätere Änderungen warten dahinter, damit nichts in falscher Reihenfolge ankommt. - Nach 24 Stunden abgeschaltet: Schlägt ein Ereignis auch nach 24 Stunden und mindestens 12 Versuchen noch fehl, schaltet Pricana den Endpunkt ab und schreibt Ihrem Team eine E-Mail. (Zeit, in der Webhooks pausierten oder der Endpunkt aus war, zählt nicht: Die Versuche gehen danach weiter.) Schalten Sie ihn im Portal wieder ein, sobald Ihr Server wieder läuft: Die Zustellung macht mit dem fehlgeschlagenen Ereignis weiter (mit neuer Ereignis-id), gefolgt von allem, was seitdem passiert ist. Sie können auch überspringen, was gewartet hat, und ab jetzt beginnen.
- Von Ihnen abgeschaltet: Ein wartendes Ereignis bleibt, wie es ist; wieder eingeschaltet, geht es zuerst hinaus, sofort.
- Was Sie noch sehen dürfen: Jeder Versuch schickt die Änderungen des Ereignisses so, wie Ihre Daten sie dann zeigen. Hat eine Website widersprochen oder ist ein Dataset inzwischen aus Ihrem Plan gefallen, fehlen seine Änderungen; ein Ereignis, von dem nichts übrig ist, gilt als zugestellt, ohne dass es geschickt wird.
- Doppelte Zustellungen: Ein Ereignis kann mehr als einmal ankommen (wenn Ihre Antwort verloren ging oder jemand es
aus dem Log erneut sendet). Jede Änderung hat ihre eigene
id: Überspringen Sie ids, die Sie schon verarbeitet haben; dieiddes Ereignisses ist bei jedem Versuch dieselbe.
Weiterleitungen (Redirects) folgt Pricana nicht: Antworten Sie unter der URL, die Sie eingetragen haben. Pricana schickt Webhooks nur an öffentliche Internetadressen.
Das Zustell-Log
API → Webhooks → Log listet jedes Ereignis der letzten 30 Tage mit seinen Versuchen, dem Statuscode Ihres Servers und dem Anfang seiner Antwort, oder mit dem, was schiefging (Timeout, Verbindung abgelehnt, TLS); seinen Body bewahren wir 7 Tage auf. Send again wiederholt ein Ereignis mit derselben id, mit dem, was Ihre Daten jetzt zeigen; ein Ereignis, das noch auf seinen nächsten Versuch wartet, versucht Try now (auch am Endpunkt) sofort, höchstens einmal pro Minute. Tests, Wiederholungen und Versuche: bis zu 30 pro Stunde.
Secret ersetzen
Roll secret erzeugt ein neues Secret. Eine Weile (standardmäßig 24 Stunden, bis zu 7 Tage) signiert Pricana mit dem
neuen und mit dem alten, der Header trägt dann zwei v1-Werte: Tragen Sie das neue Secret auf Ihrem Server ein, danach
spielt das alte keine Rolle mehr. Wählen Sie keine Überlappung, wenn das alte Secret in fremde Hände geraten sein könnte.
Wenn nichts ankommt
Webhooks pausieren, solange Ihr Plan sie nicht enthält und solange der Zugang wegen einer seit 30 Tagen überfälligen Zahlung gesperrt ist; danach geht es dort weiter, wo sie stehen geblieben sind. Das Portal zeigt, warum ein Endpunkt pausiert. Ein Endpunkt, der auf ein Dataset beschränkt ist, wird abgeschaltet, wenn dieses Dataset gelöscht wird (wählen Sie Datasets, dann schalten Sie ihn ein). Änderungen gehen etwa eine halbe Minute, nachdem Pricana sie gefunden hat, hinaus.