Skip to content

Guides

Guide: syncing with other software

A reliable sync has three parts: one full load, regular deltas, and webhooks that trigger the delta early. Writes back to STRYK use ETags so you never overwrite a change made at the front desk.

1. Initial full load

Walk all pages of each list with the maximum limit of 200. Lists are ordered by updatedAt, then id, so the cursor stays stable while data changes. Remember the newest updatedAt you have seen per resource, your high-water mark.

import { stryk } from './stryk.js';

// Initial load: walk every page, then remember the newest updatedAt you saw.
export async function fullLoad(resource, upsert) {
  let cursor = null;
  let highWaterMark = null;
  do {
    const params = new URLSearchParams({ limit: '200', ...(cursor ? { cursor } : {}) });
    const { data: page } = await stryk(`/${resource}?${params}`);
    for (const item of page.data) {
      await upsert(item);
      if (!highWaterMark || item.updatedAt > highWaterMark) highWaterMark = item.updatedAt;
    }
    cursor = page.hasMore ? page.nextCursor : null;
  } while (cursor);
  return highWaterMark; // store it per resource, e.g. in your database
}

2. Deltas with updated_since

updated_since returns everything with updatedAt greater than or equal to the given timestamp. Using the inclusive comparison means nothing changed in the same millisecond is lost; the price is that items at the mark come back. Deduplicate by id and updatedAt.

// Delta run: updated_since is inclusive (>=). Items at the mark arrive again, that is intended.
export async function syncChanges(resource, store) {
  const since = await store.highWaterMark(resource);
  let cursor = null;
  let newest = since;
  do {
    const params = new URLSearchParams({ limit: '200', updated_since: since, ...(cursor ? { cursor } : {}) });
    const { data: page } = await stryk(`/${resource}?${params}`);
    for (const item of page.data) {
      const known = await store.get(resource, item.id);
      // Deduplicate by id + updatedAt; never overwrite newer local data with an older copy.
      if (!known || item.updatedAt > known.updatedAt) await store.put(resource, item);
      if (item.updatedAt > newest) newest = item.updatedAt;
    }
    cursor = page.hasMore ? page.nextCursor : null;
  } while (cursor);
  await store.setHighWaterMark(resource, newest);
}

3. Webhooks as triggers

Webhooks make the delay short: on booking.updated, fetch that booking or run the delta for bookings. Do not build state from events alone, deliveries can be late, repeated or, after the retry schedule ends, missing.

EventRe-read
booking.created, booking.updated, booking.cancelledGET /bookings/{id}
guest.updatedGET /guests/{id}
availability.changedGET /availability for roomTypeIds, from, to
rate.changedGET /rates for the same range
payment.updatedGET /bookings/{bookingId}/payments with data.bookingId

4. Periodic reconciliation

Run the delta on a schedule even when webhooks work, every 5 to 15 minutes for bookings is typical, hourly for room types and rate plans. A nightly full load of the resources you mirror catches anything else, for example deletions in your own system.

Stay within the limit of your key (120 requests per minute by default, GET /me → rateLimit). With 200 items per page, a delta over thousands of changes takes a handful of requests; a full load spreads well over a few minutes. See Rate limits.

5. Writing with ETag and If-Match

Single resources return an ETag header and the same value as version. PATCH requires If-Match with the ETag you read. If the resource has changed in STRYK in the meantime, the write is rejected with 412 precondition_failed; without If-Match you get 428 precondition_required. If-None-Match on a GET returns 304 when nothing changed.

HTTP
GET /api/partner/v1/guests/5e4d3c2b-1a09-4f8e-9d7c-6b5a4f3e2d1c
→ 200 OK
  ETag: "7e5d3c1b"
  { "id": "5e4d3c2b-…", "phone": "+43 660 1234567", "version": "7e5d3c1b", … }

PATCH /api/partner/v1/guests/5e4d3c2b-1a09-4f8e-9d7c-6b5a4f3e2d1c
If-Match: "7e5d3c1b"
{ "phone": "+43 660 7654321" }
→ 200 OK      ETag: "9f8e7d6c"
→ 412 precondition_failed   (changed in STRYK meanwhile: read again)
→ 428 precondition_required (If-Match missing)
// Optimistic concurrency: read, change, write with If-Match.
export async function updateGuestPhone(guestId, phone) {
  for (let attempt = 1; attempt <= 3; attempt += 1) {
    const { data: guest, etag } = await stryk(`/guests/${guestId}`);
    try {
      const { data: updated } = await stryk(`/guests/${guestId}`, {
        method: 'PATCH',
        headers: { 'If-Match': etag },
        body: { phone },
      });
      return updated;
    } catch (error) {
      // 412: someone changed the guest in STRYK since your read. Read again and re-apply.
      if (error.code !== 'precondition_failed') throw error;
    }
  }
  throw new Error('Guest keeps changing; resolve manually.');
}

6. Resolving conflicts

  • STRYK is the source of truth for bookings, prices, availability and payments. Mirror them; do not correct them from the outside.
  • For guest contact data, decide per field which system wins. On 412, read again, apply only your fields, and write with the new ETag.
  • Stop after a few attempts and flag the record for a person instead of looping.
  • Never retry a 409 booking_not_modifiable: the booking is checked in, checked out or cancelled.