Anleitungen
Anleitung: eigene Buchungswebsite
Diese Anleitung baut die Serverseite einer Buchungswebsite auf der Partner API: von der Zimmerliste bis zur Bestätigungsseite und späteren Änderungen. Der Browser spricht mit deinem Backend, dein Backend mit STRYK.
Voraussetzungen
- Ein Schlüssel mit
property:read,availability:read,rates:read,bookings:readundbookings:write. Ergänzepayments:read, wenn die Bestätigungsseite den Zahlungsstand zeigt. - Ein Backend (Node.js 18+ oder Python 3.10+). Der Schlüssel bleibt dort, siehe Authentifizierung.
- Mit dem Hotel abgestimmte Testdaten. Es gibt keine Sandbox; storniere Testbuchungen sofort.
Die Beispiele nutzen eine kleine Hilfsfunktion, die den Schlüssel ergänzt, JSON sendet und Problem Details in Ausnahmen umwandelt:
// stryk.js, server-side helper (Node.js 18+). The key never reaches the browser.
const BASE_URL = 'https://app.staystryk.com/api/partner/v1';
export class StrykError extends Error {
constructor(status, problem) {
super(problem.detail ?? problem.title ?? `HTTP ${status}`);
this.status = status;
this.code = problem.code;
this.problem = problem; // RFC 9457 problem details, incl. requestId
}
}
export async function stryk(path, { method = 'GET', body, headers = {} } = {}) {
const response = await fetch(BASE_URL + path, {
method,
headers: {
Authorization: `Bearer ${process.env.STRYK_API_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
const data = response.status === 204 ? null : await response.json();
if (!response.ok) throw new StrykError(response.status, data ?? {});
return { data, etag: response.headers.get('ETag'), location: response.headers.get('Location') };
}# stryk.py, server-side helper. The key never reaches the browser.
import os
import requests
BASE_URL = "https://app.staystryk.com/api/partner/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['STRYK_API_KEY']}"
class StrykError(Exception):
def __init__(self, status, problem):
super().__init__(problem.get("detail") or problem.get("title") or f"HTTP {status}")
self.status = status
self.code = problem.get("code")
self.problem = problem # RFC 9457 problem details, incl. requestId
def stryk(path, method="GET", json=None, headers=None, params=None):
response = session.request(method, BASE_URL + path, json=json, headers=headers or {}, params=params, timeout=30)
data = None if response.status_code == 204 else response.json()
if not response.ok:
raise StrykError(response.status_code, data or {})
return data, response.headers1. Zimmerkategorien und Bilder laden
GET /room-types liefert Namen, Beschreibungen je Sprache, Belegungsgrenzen und nach position sortierte Bilder. Kategorien ändern sich selten: Cache sie und aktualisiere sie zeitgesteuert oder wenn dein Abgleich eine Änderung sieht.
import { stryk } from './stryk.js';
const { data: page } = await stryk('/room-types?limit=200');
const roomTypes = page.data
.filter((roomType) => roomType.active)
.map((roomType) => ({
id: roomType.id,
name: roomType.name,
text: roomType.description?.[locale] ?? roomType.description?.en ?? '',
maxOccupancy: roomType.maxOccupancy,
images: [...roomType.images].sort((a, b) => a.position, b.position),
}));
// Cache this list (e.g. 15 minutes) and refresh it on your sync job, not per visitor.from stryk import stryk
page, _ = stryk("/room-types", params={"limit": 200})
room_types = [
{
"id": room_type["id"],
"name": room_type["name"],
"text": (room_type.get("description") or {}).get(locale, ""),
"max_occupancy": room_type["maxOccupancy"],
"images": sorted(room_type["images"], key=lambda image: image["position"]),
}
for room_type in page["data"]
if room_type["active"]
]2. Verfügbarkeit prüfen
GET /availability liefert freie Einheiten und Restriktionen (closed, minStay, closedToArrival …) je Kategorie und Tag, für bis zu 366 Tage. Nutze das für Kalender und Suchergebnisse. Das Angebot im nächsten Schritt ist die verbindliche Prüfung für einen konkreten Aufenthalt.
// Calendar view: free units per night of the requested stay.
const params = new URLSearchParams({ from: checkIn, to: checkOut, roomTypeId });
const { data: availability } = await stryk(`/availability?${params}`);
const nights = availability.data.filter((day) => day.date < checkOut); // nights are checkIn … checkOut − 1
const bookable = nights.length > 0 && nights.every((day) => day.available > 0 && !day.closed);
const arrivalClosed = nights[0]?.closedToArrival === true;availability, _ = stryk("/availability", params={"from": check_in, "to": check_out, "roomTypeId": room_type_id})
nights = [day for day in availability["data"] if day["date"] < check_out]
bookable = bool(nights) and all(day["available"] > 0 and not day.get("closed") for day in nights)
arrival_closed = bool(nights) and nights[0].get("closedToArrival") is True3. Angebot berechnen
POST /quotes berechnet den Preis mit den Regeln des Hotels: Nachtpreise, Steuern und Abgaben, Stornobedingungen. Es wird nichts reserviert. Ist der Aufenthalt so nicht buchbar, enthält die Antwort available: false und reasons, das ist kein Fehler.
const { data: quote } = await stryk('/quotes', {
method: 'POST',
body: { checkIn, checkOut, roomTypeId, ratePlanId, adults: 2, childrenAges: [6] },
});
if (!quote.available) {
// Not an error: the stay is not bookable as requested (e.g. minimum stay).
return renderUnavailable(quote.reasons);
}
renderOffer({
total: quote.total, // { amount: "486.00", currency: "EUR" }
nightly: quote.nightly, // [{ date, price }]
taxesAndFees: quote.taxesAndFees, // e.g. tourist tax, included or not
cancellationPolicy: quote.cancellationPolicy,
});quote, _ = stryk("/quotes", method="POST", json={
"checkIn": check_in,
"checkOut": check_out,
"roomTypeId": room_type_id,
"ratePlanId": rate_plan_id,
"adults": 2,
"childrenAges": [6],
})
if not quote["available"]:
return render_unavailable(quote["reasons"]) # not an error
return render_offer(quote["total"], quote["nightly"], quote["taxesAndFees"], quote.get("cancellationPolicy"))Zeige dem Gast genau quote.total und speicher den Wert mit dem Checkout. Du sendest ihn später als expectedTotal zurück.
4. Buchung anlegen
POST /bookings verlangt einen Idempotency-Key. Erzeuge ihn je Buchungsversuch und speicher ihn mit dem Checkout: Bricht die Verbindung ab, sende dieselbe Anfrage mit demselben Schlüssel und erhalten das ursprüngliche Ergebnis statt einer zweiten Buchung.
STRYK berechnet den Preis serverseitig neu. Weicht expectedTotal vom aktuellen Preis ab, legt STRYK nichts an und antwortet mit 409 conflict und currentTotal. Ein Zimmer der gewählten Kategorie weist STRYK automatisch zu.
import { randomUUID } from 'node:crypto';
import { stryk, StrykError } from './stryk.js';
export async function createBooking(checkout) {
// One key per checkout attempt. Persist it with the checkout so a retry reuses it.
checkout.idempotencyKey ??= randomUUID();
try {
const { data: booking } = await stryk('/bookings', {
method: 'POST',
headers: { 'Idempotency-Key': checkout.idempotencyKey },
body: {
roomTypeId: checkout.roomTypeId,
ratePlanId: checkout.ratePlanId,
checkIn: checkout.checkIn,
checkOut: checkout.checkOut,
adults: checkout.adults,
childrenAges: checkout.childrenAges,
guest: {
firstName: checkout.guest.firstName,
lastName: checkout.guest.lastName,
email: checkout.guest.email,
phone: checkout.guest.phone,
},
notes: checkout.notes,
externalReference: `WEB-${checkout.id}`,
expectedTotal: checkout.quote.total, // the price the guest saw
},
});
return { status: 'booked', booking };
} catch (error) {
if (!(error instanceof StrykError)) throw error; // network error: retry with the same key
switch (error.code) {
case 'not_available':
return { status: 'sold_out' }; // reload availability, offer alternatives
case 'conflict':
if (error.problem.currentTotal) {
checkout.idempotencyKey = undefined; // the next attempt is a new request
return { status: 'price_changed', currentTotal: error.problem.currentTotal };
}
throw error;
case 'idempotency_in_progress':
return { status: 'pending' }; // wait a moment, then retry with the same key
default:
throw error; // log error.problem.requestId
}
}
}import uuid
from stryk import stryk, StrykError
def create_booking(checkout):
# One key per checkout attempt. Persist it so a retry reuses it.
checkout.setdefault("idempotency_key", str(uuid.uuid4()))
try:
booking, _ = stryk("/bookings", method="POST", headers={"Idempotency-Key": checkout["idempotency_key"]}, json={
"roomTypeId": checkout["room_type_id"],
"ratePlanId": checkout["rate_plan_id"],
"checkIn": checkout["check_in"],
"checkOut": checkout["check_out"],
"adults": checkout["adults"],
"childrenAges": checkout["children_ages"],
"guest": checkout["guest"], # firstName, lastName, email, phone
"notes": checkout.get("notes"),
"externalReference": f"WEB-{checkout['id']}",
"expectedTotal": checkout["quote"]["total"], # the price the guest saw
})
return {"status": "booked", "booking": booking}
except StrykError as error:
if error.code == "not_available":
return {"status": "sold_out"}
if error.code == "conflict" and "currentTotal" in error.problem:
checkout.pop("idempotency_key") # the next attempt is a new request
return {"status": "price_changed", "current_total": error.problem["currentTotal"]}
if error.code == "idempotency_in_progress":
return {"status": "pending"} # wait, then retry with the same key
raise # log error.problem["requestId"]{
"id": "9d3c1b2a-7e6f-4a5b-8c9d-0e1f2a3b4c5d",
"reference": "B-2026-01482",
"status": "confirmed",
"channel": "api",
"externalReference": "WEB-1234",
"checkIn": "2026-12-01",
"checkOut": "2026-12-04",
"nights": 3,
"adults": 2,
"children": 1,
"childrenAges": [6],
"rooms": [{ "roomTypeId": "0b7e2c1d-5f4a-4c3b-9e8d-7a6b5c4d3e2f", "adults": 2, "children": 1 }],
"guest": { "id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a4f3e2d1c", "firstName": "Anna", "lastName": "Berger", "email": "anna.berger@example.com" },
"total": { "amount": "486.00", "currency": "EUR" },
"paid": { "amount": "0.00", "currency": "EUR" },
"outstanding": { "amount": "486.00", "currency": "EUR" },
"createdAt": "2026-11-04T08:15:00.000Z",
"updatedAt": "2026-11-04T08:15:00.000Z",
"version": "1a2b3c4d"
}5. Ausgebuchte Zimmer und Preisänderungen behandeln
| Antwort | Bedeutung | Was deine Website tut |
|---|---|---|
409 not_available | Das letzte freie Zimmer wurde inzwischen gebucht oder eine Restriktion greift. | Gast informieren, Verfügbarkeit neu laden, andere Daten oder Kategorien anbieten. |
409 conflict mit currentTotal | Der Preis hat sich seit dem Angebot geändert. | Neuen Preis zeigen und bestätigen lassen. Der bestätigte Versuch ist eine neue Anfrage mit neuem Idempotency-Key. |
409 idempotency_in_progress | Die erste Anfrage mit diesem Schlüssel läuft noch. | Eine Sekunde warten und mit demselben Schlüssel wiederholen. |
422 validation_failed | Ein Feld ist ungültig; errors[] nennt es. | Meldung am betroffenen Feld anzeigen. |
Timeout, 5xx | Unklar, ob die Buchung existiert. | Mit demselben Schlüssel wiederholen. Du erhältst das gespeicherte Ergebnis der ersten Anfrage. |
{
"type": "https://staystryk.com/developers/errors#conflict",
"title": "Conflict",
"status": 409,
"detail": "The price of this stay has changed.",
"instance": "/api/partner/v1/bookings",
"code": "conflict",
"requestId": "4c3b2a19-0f8e-4d7c-b6a5-948372615049",
"currentTotal": { "amount": "512.00", "currency": "EUR" }
}6. Bestätigung anzeigen
Lies die Buchung zurück und zeige, was STRYK gespeichert hat: die Buchungsnummer in reference, den Status, den Aufenthalt und den offenen Saldo in outstanding.
// Confirmation page: show what STRYK stored, not what the form contained.
const { data: booking } = await stryk(`/bookings/${bookingId}`);
renderConfirmation({
reference: booking.reference, // the hotel's booking number
status: booking.status, // confirmed | tentative | pending …
stay: [booking.checkIn, booking.checkOut, booking.nights],
total: booking.total,
outstanding: booking.outstanding, // paid at the hotel / via the hotel's payment path
});7. Spätere Änderungen per Webhook verfolgen
Die Rezeption ändert vielleicht Daten, ergänzt eine Notiz oder storniert. Registriere in STRYK einen Webhook-Endpunkt für booking.updated und booking.cancelled und lies die Buchung, wenn ein Ereignis eintrifft. Ereignisse sind bewusst schlank: du sagst, was sich geändert hat; die API sagt, wie es jetzt aussieht.
// Worker: idempotent by event id, fetches the current state instead of trusting the event.
export async function handleEvent(event, db) {
if (await db.processedEvents.has(event.id)) return; // duplicate delivery
switch (event.type) {
case 'booking.created':
case 'booking.updated':
case 'booking.cancelled': {
const { data: booking } = await stryk(`/bookings/${event.data.id}`);
await db.bookings.upsertIfNewer(booking); // compare updatedAt
break;
}
case 'availability.changed':
await refreshAvailability(event.data.roomTypeIds, event.data.from, event.data.to);
break;
case 'webhook.test':
break; // sent from STRYK on request
}
await db.processedEvents.add(event.id);
}Signaturprüfung, Wiederholungen und Zustellprotokoll: Webhooks.
8. Buchung stornieren
POST /bookings/{id}/cancel storniert über den normalen Stornoablauf von STRYK. Stornobedingungen und mögliche Gebühren des Hotels gelten genau wie bei einer Stornierung an der Rezeption. Ein Idempotency-Key ist Pflicht; ein aus der Buchungs-ID abgeleiteter Schlüssel macht doppelte Klicks harmlos.
const { data: cancelled } = await stryk(`/bookings/${bookingId}/cancel`, {
method: 'POST',
headers: { 'Idempotency-Key': `cancel-${bookingId}` },
body: { reason: 'Cancelled by guest on the hotel website' },
});
// cancelled.status === 'cancelled'; cancelled.cancellation.cancelledAt
// The hotel's cancellation policy and fees apply, exactly as in STRYK.cancelled, _ = stryk(
f"/bookings/{booking_id}/cancel",
method="POST",
headers={"Idempotency-Key": f"cancel-{booking_id}"},
json={"reason": "Cancelled by guest on the hotel website"},
)
assert cancelled["status"] == "cancelled"Eingecheckte, ausgecheckte oder bereits stornierte Buchungen erhalten 409 booking_not_modifiable.
Checkliste vor dem Livegang
- Der Schlüssel liegt nur auf dem Server und hat genau die oben genannten Scopes.
- Jeder Buchungsversuch hat einen eigenen gespeicherten
Idempotency-Key; Wiederholungen verwenden ihn erneut. - Der Gast sieht den kalkulierten Preis, und
expectedTotalwird mitgesendet. not_availableundconflictführen zu einer klaren Meldung, nicht zu einer Fehlerseite.- Webhook-Signaturen werden am Roh-Body geprüft; Ereignisse werden je
ideinmal verarbeitet. - Die Website nennt, wie der offene Saldo bezahlt wird.
- Alle Testbuchungen sind storniert.