Zum Inhalt springen
Passavo
Menü

Bauen Sie Ihre eigene Anbindung

Ihren Katalog, Ihre freien Plätze, Ihre Bestellungen, Ihre Tickets und Ihre Scans kann auch ein anderes Programm abrufen, und Bestellungen kann es selbst anlegen. Hier steht, wie das geht — erst in einfachen Worten, dann mit dem Code.

Basisadresse
https://passavo.eu/api/v1
Aktuelle Version
2026-09-20
Die Beschreibung selbst
openapi.json

Was ist eine API, und brauchen Sie eine?

Eine API ist eine Tür an der Rückseite Ihres Kontos. Wo Sie im Verwaltungsbildschirm klicken, kann ein Programm durch diese Tür Ihre Daten abrufen oder hineinschreiben — ohne dass jemand vor einem Bildschirm sitzt.

Sie brauchen sie nur, wenn Sie etwas wollen, was der Verwaltungsbildschirm nicht kann: Zahlen im eigenen System, freie Plätze auf der eigenen Website, Tickets aus einer Kasse, die Sie schon haben. Wenn Sie es klicken können, ist Klicken schneller.

Wer diese Anbindung baut — Ihr Webbauer, Ihr Buchhaltungsprogramm, jemand, der programmieren kann — braucht zwei Dinge von Ihnen: die Adresse unten und einen Schlüssel, den Sie selbst anlegen.

Was Sie damit bauen können

Freie Plätze auf der eigenen Website

Zeigen Sie auf Ihrer Startseite, welche Führungen diese Woche noch Plätze haben, in Ihrer eigenen Gestaltung, mit Zahlen, die in diesem Moment stimmen.

Ihre Buchhaltung versorgen

Lassen Sie Ihren Steuerberater oder Ihr Buchhaltungsprogramm den Umsatz des Vormonats selbst abholen, statt jeden Monat einen Export zu verschicken.

Ein Bildschirm am Eingang

Ein Tablet in der Eingangshalle, das zeigt, welches Zeitfenster gleich beginnt und wie viele Plätze noch frei sind. Ein Aufruf pro Minute genügt.

Aus dem eigenen System verkaufen

Legen Sie eine Bestellung aus dem Programm an, in dem Sie ohnehin arbeiten, schicken Sie dem Käufer einen Zahllink und erfahren Sie per Webhook, dass er bezahlt hat.

Die API gehört zum Tarif Pro

Schlüssel legen Sie unter Einstellungen → API-Zugang an. Dieser Bildschirm erscheint in Ihrem Menü, sobald Ihre Organisation auf diesem Tarif ist; den Tarif wechseln Sie selbst im Verwaltungsbildschirm unter Abonnement.

Tarife ansehen →

Erst gefahrlos ausprobieren

Neben einem normalen Schlüssel (pv_live_) können Sie einen Testschlüssel anlegen (pv_test_). Er arbeitet mit Ihrem echten Katalog, Ihren Zeitfenstern und Ihrer Kapazität und kann auch Bestellungen anlegen. Eine solche Testbestellung läuft nie über einen Zahlungsanbieter — eine Testseite spielt die Zahlung nach —, zählt nicht zu Ihrem Umsatz und wird nach 24 Stunden aufgeräumt. Bis dahin hält sie echte Plätze fest; arbeiten Sie also am besten mit einem Produkt, das Sie dafür anlegen. Was außerhalb der Sandbox liegt, etwa Webhooks verwalten, weisen wir bei einem Testschlüssel mit dem Code sandbox ab.

Schnellstart in fünf Schritten

Von nichts zu einer Anbindung, die weiß, dass bezahlt wurde. Jeder Schritt dauert Minuten, nicht Tage.

  1. Legen Sie einen Schlüssel an

    Öffnen Sie im Verwaltungsbildschirm Einstellungen → API-Zugang und legen Sie einen Schlüssel mit genau den Rechten an, die Sie brauchen. Das vollständige Token sehen Sie einmal; bewahren Sie es wie ein Passwort auf.

  2. Machen Sie Ihren ersten Aufruf

    Rufen Sie GET /me mit Ihrem Schlüssel im Authorization-Header ab. Sie bekommen Ihre Organisation, Ihren Tarif und die Rechte Ihres Schlüssels zurück. Klappt das, klappt auch der Rest.

  3. Legen Sie eine Bestellung an

    Schicken Sie die Zeilen, die Sie verkaufen wollen, an POST /orders, mit einem Idempotency-Key — der ist hier Pflicht. Bricht die Verbindung unterwegs ab, ergibt derselbe Aufruf noch einmal keine zweite Bestellung. Das Feld reserved_until sagt, bis wann die Plätze für Sie festgehalten werden.

  4. Schicken Sie den Käufer zum Zahllink

    Fordern Sie mit POST /orders/{id}/checkout den Zahlungslink an, zusammen mit der Adresse, auf der der Käufer nach dem Bezahlen landet. Schicken Sie den Käufer zu diesem Link oder zeigen Sie ihn in Ihrer eigenen Seite. Die Zahlung läuft über den Anbieter, den Sie bereits verbunden haben; sobald sie eingegangen ist, werden die Tickets erstellt und verschickt.

  5. Lassen Sie einen Webhook Bescheid geben

    Richten Sie eine Adresse auf Ihrer Website ein und melden Sie sie an. Sobald bezahlt, storniert oder gescannt wurde, kommt das dort an — Sie müssen nicht ständig nachfragen, ob schon etwas passiert ist.

curl

curl "https://passavo.eu/api/v1/me" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/me');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/me', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/me", headers=headers)
data = response.json()["data"]

Authentifizierung

Jede Anfrage trägt Ihren Schlüssel als Bearer-Token. Ein Schlüssel gehört zu genau einer Organisation: diese Organisation ist die ganze Welt dieser Anfrage, und Sie können damit niemals versehentlich Daten eines anderen abrufen.

Authorization: Bearer pv_live_YOUR_API_KEY

Ein normaler Schlüssel beginnt mit pv_live_, ein Sandbox-Schlüssel mit pv_test_. Dieses Präfix ist kein Geheimnis, sondern eine Hilfe: wer es in einem Protokoll sieht, weiß sofort, woher es kommt.

Das vollständige Token besteht einen Moment, beim Anlegen. Danach bewahren wir nur einen nicht umkehrbaren Abdruck (sha256) davon auf. Weg ist also wirklich weg: ziehen Sie den Schlüssel zurück und legen Sie einen neuen an.

Rechte

Ein Schlüssel ohne Rechte darf nichts. Das ist bewusst das Gegenteil dessen, was man erwarten würde: diese Schlüssel werden von Kunden an fremde Werkzeuge weitergegeben, und dann muss die Voreinstellung „nichts“ sein und nicht „alles“.

read
Alles abrufen: Ihre Standorte, Produkte, Preise, freien Plätze, Rabatte, Gutscheine und Jahreskarten, dazu Ihre Bestellungen, Tickets, Käufer und die Scans am Eingang.
write
Bestellungen anlegen, bezahlen lassen, stornieren und erstatten; Tickets entwerten oder erneut senden; Besucher einchecken; Gutscheine und Jahreskarten ausgeben.
webhooks
Eigene Webhook-Ziele verwalten.
external_payment
Eine Bestellung, die Geld kostet, ohne Zahlungsanbieter als bezahlt markieren, etwa nach einer Überweisung oder Barzahlung (POST /orders/{id}/mark-paid). Ein eigenes Recht neben write, damit Sie es nur einer Kasse oder Buchhaltung geben, die Sie selbst verwalten. Eine Bestellung über null Euro bestätigen geht auch ohne.

Wenn etwas schiefgeht

Jeder Fehler hat dieselbe Form, mit einem festen Code und einem übersetzten Text. Lesen Sie den Code und nicht den Text: der Text darf umgeschrieben werden, der Code nie.

{
  "error": {
    "code": "validation_failed",
    "message": "…",
    "details": {}
  }
}
Code Status Wann
unauthenticated 401 Kein, unbekannter, zurückgezogener oder abgelaufener Schlüssel.
forbidden 403 Der Schlüssel stimmt, darf das aber nicht.
plan_required 403 Der Tarif dieser Organisation enthält die API nicht.
sandbox 403 Ein Testschlüssel versucht etwas außerhalb der Sandbox, etwa Webhooks zu verwalten.
not_found 404 Gibt es nicht, oder nicht innerhalb dieser Organisation.
validation_failed 422 Die Anfrage selbst stimmt nicht: ein unbekannter Filter, ein ungültiges Datum.
rate_limited 429 Zu viele Aufrufe in einer Minute.
conflict 409 Derselbe Idempotency-Key wurde schon für etwas anderes benutzt.
server_error 500 Auf unserer Seite ist etwas schiefgegangen.
slot_unavailable 409 Das Zeitfenster ist voll, hat bereits begonnen oder ist unbekannt, oder es wurde keines gewählt, obwohl das Produkt eines verlangt.
sold_out 409 Das Produkt ist ausverkauft oder nicht mehr im Verkauf.
discount_invalid 422 Der Rabattcode existiert nicht, ist aufgebraucht oder gilt hier nicht.
payment_provider_missing 409 Die Organisation hat keinen Zahlungsanbieter verknüpft.
not_cancellable 409 Die Bestellung kann in ihrem aktuellen Zustand nicht storniert werden.
not_refundable 409 Bei dieser Bestellung gibt es nichts (mehr) zu erstatten, oder sie wurde außerhalb des Zahlungsanbieters bezahlt.
check_in_duplicate 409 Dieser Code wurde bereits eingecheckt.
check_in_invalid 404 Dieser Code ist unbekannt.
check_in_wrong_day 409 Dieser Code ist gültig, aber nicht heute.
check_in_cancelled 409 Dieser Code gehört zu einem stornierten oder erstatteten Ticket.

Eine Zeile einer anderen Organisation ergibt immer not_found und nie eine Meldung, dass Sie nicht dürfen. Wäre dieser Unterschied sichtbar, könnte jeder mit einem gültigen Schlüssel durch Abzählen von Nummern herausfinden, wie viele Produkte der Nachbar hat.

Blättern, sortieren und filtern

Listen kommen seitenweise. Sie fragen nicht nach Seite drei, sondern nach dem, was nach der vorigen Seite kommt: die Antwort trägt einen Cursor, den Sie mitschicken, um weiterzulesen. So überspringt eine Liste nie Zeilen, wenn während des Blätterns etwas hinzukommt.

Parameter Was es tut
page[size] Wie viele Zeilen pro Seite; standardmäßig 25, höchstens 100.
page[cursor] Der next_cursor aus der vorigen Antwort. Ist er leer, war das die letzte Seite.
sort Wonach sortiert wird. Ein Strich davor dreht die Reihenfolge um.
filter[…] Nach einem Feld filtern, das der Endpunkt zulässt.
include Beziehungen in derselben Antwort mitnehmen, durch Kommas getrennt.

Ein Parameter, den der Endpunkt nicht kennt, ist ein Fehler und kein Schweigen. Wer sich im Namen eines Filters vertippt, soll das hören — und nicht unbemerkt die ganze Liste ungefiltert zurückbekommen.

In der Antwort steht bewusst keine Gesamtzahl: bei Zehntausenden Zeilen kostet dieses Zählen mehr als die Seite selbst.

{
  "data": [ … ],
  "meta": { "next_cursor": "…" }
}

Zweimal senden, einmal ausführen

Schicken Sie bei jedem Aufruf, der etwas anlegt oder ändert, einen Idempotency-Key mit — eine zufällige, eindeutige Zeichenfolge. Kommt derselbe Aufruf mit demselben Schlüssel noch einmal an, bekommen Sie genau dieselbe Antwort zurück, ohne dass etwas ein zweites Mal geschieht. Bei POST /orders ist er Pflicht.

Idempotency-Key: 6f1c0b4a-6b2f-4a1b-9c7e-1f2d3e4a5b6c

Derselbe Schlüssel mit anderem Inhalt ergibt conflict, und es geschieht nichts. Ein gespeicherter Schlüssel hält 24 Stunden und gilt pro Organisation.

Versionen

Die Version steht im Pfad (/api/v1) und ändert sich nur bei einem Bruch; eine zweite Version läuft dann daneben und nicht hindurch. Jede kleine Ergänzung wird im Header Passavo-Version datiert, derzeit 2026-09-20.

Was innerhalb dieser Version hinzukommen darf: ein Feld, ein Endpunkt, ein neuer Sortier- oder Filterwert, ein neuer Fehlercode. Bauen Sie Ihre Anbindung also so, dass ein unbekanntes Feld sie nicht umwirft.

Was innerhalb dieser Version nie geschieht: ein Feld entfernen oder umbenennen, einen Typ ändern, einem Code eine andere Bedeutung geben oder einen Parameter verpflichtend machen.

Aufrufgrenze

Ein Schlüssel darf 120 Aufrufe pro Minute machen. Die Grenze gilt pro Schlüssel und nicht pro Adresse, damit sich zwei Kunden hinter demselben Cloud-Anbieter nicht gegenseitig behindern.

Jede Antwort sagt Ihnen in X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, wo Sie stehen. Liegen Sie darüber, bekommen Sie rate_limited mit einem Retry-After dazu; warten Sie dann einfach so viele Sekunden.

Codebeispiele

Dieselbe Anfrage in vier Sprachen, je Gruppe von Endpunkten. Ersetzen Sie YOUR_API_KEY durch Ihren eigenen Schlüssel — und schreiben Sie ihn nie in Code, den ein Besucher herunterladen kann.

Konto

Wer bin ich und was darf ich.

GET /api/v1/me

curl

curl "https://passavo.eu/api/v1/me" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/me');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/me', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/me", headers=headers)
data = response.json()["data"]

Katalog

Standorte, Produkte und Preise.

GET /api/v1/events

curl

curl "https://passavo.eu/api/v1/events" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/events');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/events', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/events", headers=headers)
data = response.json()["data"]

Verfügbarkeit

Zeitfenster und freie Plätze.

GET /api/v1/products/{id}/slots

curl

curl "https://passavo.eu/api/v1/products/12/slots" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/products/12/slots');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/products/12/slots', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/products/12/slots", headers=headers)
data = response.json()["data"]

Verkauf

Rabatte, Gutscheine und Jahreskarten.

GET /api/v1/discounts

curl

curl "https://passavo.eu/api/v1/discounts" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/discounts');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/discounts', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/discounts", headers=headers)
data = response.json()["data"]

Bestellungen

Bestellungen anlegen, bezahlen, stornieren und erstatten.

GET /api/v1/orders

curl

curl "https://passavo.eu/api/v1/orders" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/orders');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/orders', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/orders", headers=headers)
data = response.json()["data"]

Tickets

Tickets abrufen, entwerten und erneut versenden.

GET /api/v1/tickets

curl

curl "https://passavo.eu/api/v1/tickets" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/tickets');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/tickets', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/tickets", headers=headers)
data = response.json()["data"]

Eingang

Einchecken am Einlass.

GET /api/v1/check-ins

curl

curl "https://passavo.eu/api/v1/check-ins" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/check-ins');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/check-ins', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/check-ins", headers=headers)
data = response.json()["data"]

Kunden

Käufer, zusammengefasst aus den Bestellungen.

GET /api/v1/customers

curl

curl "https://passavo.eu/api/v1/customers" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/customers');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/customers', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/customers", headers=headers)
data = response.json()["data"]

Meta

Die Beschreibung der API selbst.

GET /api/v1/openapi.json

curl

curl "https://passavo.eu/api/v1/openapi.json" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->get('https://passavo.eu/api/v1/openapi.json');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/openapi.json', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json"}
response = requests.get("https://passavo.eu/api/v1/openapi.json", headers=headers)
data = response.json()["data"]

Webhooks

Nachrichten, die wir an Ihren Server senden.

GET /api/v1/webhook-deliveries

curl

curl "https://passavo.eu/api/v1/webhook-deliveries" \
  -H "Authorization: Bearer pv_live_YOUR_API_KEY" \
  -H "Accept: application/json"

PHP

use Illuminate\Support\Facades\Http;

$response = Http::acceptJson()
    ->withToken('pv_live_YOUR_API_KEY')
    ->get('https://passavo.eu/api/v1/webhook-deliveries');

$data = $response->json('data');

JavaScript

const response = await fetch('https://passavo.eu/api/v1/webhook-deliveries', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer pv_live_YOUR_API_KEY',
  },
});

const { data } = await response.json();

Python

import requests

headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/webhook-deliveries", headers=headers)
data = response.json()["data"]

Webhooks

Ein Webhook ist die Umkehrung eines Aufrufs: statt dass Sie immer wieder fragen, ob schon bezahlt wurde, schicken wir eine Nachricht an eine Adresse von Ihnen, sobald es geschieht. Das spart Tausende Aufrufe am Tag, und Sie wissen es sofort statt bei der nächsten Runde.

Ihre Ziele verwalten Sie im Verwaltungsbildschirm unter Einstellungen → API-Zugang, Reiter Webhooks, oder über die API mit einem Schlüssel, der das Recht webhooks trägt. Jedes Ziel hat ein eigenes Geheimnis; damit prüfen Sie nach, dass eine Nachricht wirklich von uns kommt.

Die Signatur nachrechnen

Jede Nachricht trägt den Header Passavo-Signature in der Form t=<unix>,v1=<hmac-sha256>. t ist der Moment, in dem wir signiert haben; v1 ist ein HMAC-SHA256 mit dem Geheimnis dieses Ziels über den Zeitstempel, einen Punkt und den rohen Body. Rechnen Sie ihn selbst nach und vergleichen Sie in konstanter Zeit — ein gewöhnlicher Vergleich verrät über seine Dauer, wie viele Zeichen gestimmt haben. Weisen Sie außerdem eine Nachricht ab, deren t um mehr als 300 Sekunden von Ihrer eigenen Uhr abweicht: So kann niemand eine abgefangene Nachricht später erneut abspielen.

Prüfen Sie gegen den ROHEN Body, bevor Ihr Framework json daraus macht. Json neu zusammenzusetzen bringt die Schlüssel in eine andere Reihenfolge, und dann stimmt keine Signatur mehr.

curl

# De handtekening narekenen vanaf de opdrachtregel.
# T is de waarde van t= uit de header Passavo-Signature,
# het resultaat hoort gelijk te zijn aan de waarde van v1=.
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"

PHP

// De RUWE body, niet $request->all().
$body = $request->getContent();

// "t=1758355200,v1=9f86d0…" uit elkaar halen.
$delen = [];
foreach (explode(',', (string) $request->header('Passavo-Signature')) as $stuk) {
    [$sleutel, $waarde] = array_pad(explode('=', trim($stuk), 2), 2, '');
    $delen[$sleutel] = $waarde;
}

$t = $delen['t'] ?? '';
$verwacht = hash_hmac('sha256', $t . '.' . $body, $secret);

// hash_equals vergelijkt in constante tijd; de tijdstempel
// houdt een opgevangen bericht tegen dat later opnieuw komt.
if (! ctype_digit($t) || abs(time() - (int) $t) > 300 || ! hash_equals($verwacht, $delen['v1'] ?? '')) {
    abort(400);
}

JavaScript

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: de ruwe body als string, bv. via express.raw().
const header = request.headers['passavo-signature'] ?? '';
const delen = Object.fromEntries(header.split(',').map((s) => s.trim().split('=', 2)));

const verwacht = createHmac('sha256', secret).update(delen.t + '.' + rawBody).digest('hex');
const gekregen = delen.v1 ?? '';

const vers = /^\d+$/.test(delen.t ?? '')
  && Math.abs(Date.now() / 1000 - Number(delen.t)) <= 300;
const geldig = vers
  && verwacht.length === gekregen.length
  && timingSafeEqual(Buffer.from(verwacht), Buffer.from(gekregen));

Python

import hashlib, hmac, time

# raw_body: de ruwe body als bytes, bv. request.get_data() in Flask.
header = request.headers.get("Passavo-Signature", "")
delen = dict(stuk.strip().split("=", 1) for stuk in header.split(",") if "=" in stuk)
t = delen.get("t", "")

verwacht = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()

# compare_digest vergelijkt in constante tijd.
geldig = (t.isdigit()
          and abs(time.time() - int(t)) <= 300
          and hmac.compare_digest(verwacht, delen.get("v1", "")))

Die vollständige Liste der Ereignisse und ihres Inhalts steht in der Referenz. →

Was sich ändert

Jede Änderung, die es wert ist, genannt zu werden, bekommt eine neue datierte Version. Bleibt Ihre Anbindung auf einem älteren Datum stehen, funktioniert sie weiter — das Datum sagt nur, gegen welches Verhalten sie gebaut wurde.

  1. 2026-09-20

    Die erste Version

    Schlüssel mit Rechten, eine Sandbox, ein einziges Antwortformat und Cursor-Blättern. Lesen: Ihre Organisation, Standorte, Produkte, Zeitfenster, Veranstaltungen, Rabatte, Gutscheine und Jahreskarten, Bestellungen, Tickets, Käufer und Scans. Schreiben: Bestellungen anlegen, bezahlen, stornieren und erstatten, Tickets entwerten, Besucher einchecken, Gutscheine und Jahreskarten ausgeben. Dazu ausgehende Webhooks, pro Ziel signiert.