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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.