Skip to content

Guides

Guide: your own booking website

This guide builds the server side of a booking website on the Partner API: from the room list to the confirmation page and later changes. The browser talks to your backend; your backend talks to STRYK.

Before you start

  • A key with property:read, availability:read, rates:read, bookings:read and bookings:write. Add payments:read if the confirmation page shows the payment status.
  • A backend (Node.js 18+ or Python 3.10+). The key stays there, see Authentication.
  • Agreement with the hotel on test dates. There is no sandbox; cancel test bookings straight away.

The examples use a small helper that adds the key, sends JSON and turns problem details into exceptions:

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. Load room types and images

GET /room-types returns names, descriptions per language, occupancy limits and images sorted by position. Room types change rarely: cache them and refresh on a schedule or when your sync job sees a change.

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

GET /availability returns free units and restrictions (closed, minStay, closedToArrival …) per room type and day, for up to 366 days. Use it for calendars and search results. The quote in the next step is the binding check for one concrete stay.

// 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. Get a quote

POST /quotes calculates the price with the hotel's own rules: nightly prices, taxes and fees, cancellation policy. It does not reserve anything. If the stay cannot be booked as requested, the response has available: false and reasons, that is not an error.

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

Show the guest exactly quote.total and keep it with the checkout. You will send it back as expectedTotal.

4. Create the booking

POST /bookings needs an Idempotency-Key. Generate one per checkout attempt and store it with the checkout: if the connection drops, you send the same request with the same key and receive the original result instead of a second booking.

STRYK prices the booking again on the server. If expectedTotal differs from the current price, STRYK creates nothing and answers 409 conflict with currentTotal. STRYK assigns a room of the requested type automatically.

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. Handle sold-out rooms and price changes

ResponseMeaningWhat your website does
409 not_availableThe last free room was booked in the meantime, or a restriction applies.Tell the guest, reload availability, offer other dates or room types.
409 conflict with currentTotalThe price has changed since the quote.Show the new price and ask for confirmation. The confirmed attempt is a new request with a new Idempotency-Key.
409 idempotency_in_progressThe first request with this key is still running.Wait a second and retry with the same key.
422 validation_failedA field is invalid; errors[] names it.Show the message next to the field.
Timeout, 5xxUnknown whether the booking exists.Retry with the same key. You get the stored result of the first request.
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. Show the confirmation

Read the booking back and show what STRYK stored: the booking number in reference, the status, the stay and the open balance 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. Follow later changes with webhooks

The front desk may change dates, add a note or cancel. Register a webhook endpoint in STRYK for booking.updated and booking.cancelled and fetch the booking when an event arrives. Events are thin by design: they say what changed, the API says how it looks now.

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);
}

Signature check, retries and delivery log: Webhooks.

8. Cancel a booking

POST /bookings/{id}/cancel cancels through STRYK's normal cancellation process. The hotel's cancellation policy and any fees apply exactly as for a cancellation at the front desk. An Idempotency-Key is required; a key derived from the booking ID makes repeated clicks harmless.

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.

Bookings that are checked in, checked out or already cancelled return 409 booking_not_modifiable.

Checklist before go-live

  • The key is only on the server, with exactly the scopes listed above.
  • Every booking attempt has its own stored Idempotency-Key; retries reuse it.
  • The guest sees the quoted price, and expectedTotal is sent.
  • not_available and conflict lead to a clear message, not an error page.
  • Webhook signatures are verified on the raw body; events are processed once per id.
  • The website states how the open balance is paid.
  • All test bookings are cancelled.