Zum Inhalt springen

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:

JSON
{
  "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-Signaturet=<unix seconds>,v1=<signature> (unten)
Pricana-Event-Iddie id des Ereignisses: bei jedem Versuch dieselbe
Pricana-Event-Typechanges, oder ping für einen Test
Pricana-Delivery-Attempt1 beim ersten Versuch, dann 2, 3 …
User-AgentPricanaWebhooks/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.

  1. Lesen Sie t und jedes v1 aus dem Header (nachdem ein Secret ersetzt wurde, kann es zwei v1 geben).
  2. Berechnen Sie HMAC-SHA256(secret, t + "." + body) über den Body genau so, wie er angekommen ist, bevor Sie ihn parsen.
  3. Akzeptieren Sie ihn, wenn das Ergebnis einem der v1-Werte entspricht (in konstanter Zeit verglichen) und t höchstens 5 Minuten alt ist.

Python

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 "", 204

Node.js

JavaScript
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
<?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

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; die id des 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.