Zum Inhalt springen

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:read und bookings:write. Ergänze payments: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
// 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') };
}

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

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;

3. 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,
});

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
    }
  }
}
Response, 201
{
  "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

AntwortBedeutungWas deine Website tut
409 not_availableDas letzte freie Zimmer wurde inzwischen gebucht oder eine Restriktion greift.Gast informieren, Verfügbarkeit neu laden, andere Daten oder Kategorien anbieten.
409 conflict mit currentTotalDer 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_progressDie erste Anfrage mit diesem Schlüssel läuft noch.Eine Sekunde warten und mit demselben Schlüssel wiederholen.
422 validation_failedEin Feld ist ungültig; errors[] nennt es.Meldung am betroffenen Feld anzeigen.
Timeout, 5xxUnklar, ob die Buchung existiert.Mit demselben Schlüssel wiederholen. Du erhältst das gespeicherte Ergebnis der ersten Anfrage.
Response, 409
{
  "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.

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

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

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 expectedTotal wird mitgesendet.
  • not_available und conflict führen zu einer klaren Meldung, nicht zu einer Fehlerseite.
  • Webhook-Signaturen werden am Roh-Body geprüft; Ereignisse werden je id einmal verarbeitet.
  • Die Website nennt, wie der offene Saldo bezahlt wird.
  • Alle Testbuchungen sind storniert.