Skip to content
StippaStippa
FeaturesPricingDemoFAQContact
Log inGet started

Getting started

  • Introduction
  • Authentication
  • Permissions

Core concepts

  • Errors
  • Pagination
  • Idempotency
  • Rate limits

Resources

  • Appointments
  • Customers
  • Services
  • Staff
  • Availability
  • Products
  • Payments
  • Webhooks

Recipes

  • Recipe: book from your own website
  • Recipe: sync the diary into a spreadsheet or BI tool
  • Recipe: a Slack message on every new booking

Reference

  • Changelog
  • OpenAPI 3.1 document

Pagination

Cursor pagination, why it is the only kind here, and a loop that walks a whole list.

Last updated 1 August 2026

Every list endpoint returns the same envelope:

{
  "data": [ { "id": "..." } ],
  "hasMore": true,
  "nextCursor": "eyJpZCI6IjljYjEuLi4iLCJ0IjoiMjAyNi0wOC0wMVQwOTozMDowMFoifQ"
}

Two query parameters control it:

ParameterDefaultNotes
limit501 to 100. Asking for more is a 400, not a silent clamp
cursornoneThe nextCursor from the previous page. Omit it for the first page

Walking a list

# First page
curl "https://stippa.nl/api/v1/appointments?limit=100" \
  -H "Authorization: Bearer $STIPPA_KEY"

# Next page
curl "https://stippa.nl/api/v1/appointments?limit=100&cursor=eyJpZCI6..." \
  -H "Authorization: Bearer $STIPPA_KEY"
async function* everyAppointment(key, params = {}) {
  let cursor = null;

  do {
    const query = new URLSearchParams({ ...params, limit: '100' });
    if (cursor) query.set('cursor', cursor);

    const response = await fetch(`https://stippa.nl/api/v1/appointments?${query}`, {
      headers: { Authorization: `Bearer ${key}` },
    });

    if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);

    const page = await response.json();
    yield* page.data;

    cursor = page.nextCursor;
  } while (cursor);
}
💡

Loop on nextCursor rather than on hasMore. They agree, but nextCursor is the value you need anyway, and a loop that reads one field cannot get the two out of step.

The last page has hasMore: false and nextCursor: null. An empty result set is a normal 200 with data: [], never a 404.

Cursors are opaque

A cursor is a string we minted. Do not construct one, parse one, or store one as anything other than the exact string you received.

The current implementation happens to be base64, and that is not a licence to decode it. It is opaque by contract precisely so the mechanism underneath can change without a version bump, and a client that decodes it will break on a change we are not obliged to announce.

A cursor we cannot read is a 400 VALIDATION_ERROR naming cursor in fields. The only recovery is to start the walk again without one.

Why there is no page number

There is no ?page=2, and there will not be. Offset pagination is specifically wrong for this data.

An appointment list changes continuously while you are reading it: bookings arrive, cancellations land, a business reschedules somebody. With an offset, a row inserted before your current position shifts everything down by one, so page 2 skips a record and page 3 shows one twice. You get a corrupt export and no error.

A cursor names the last row you saw rather than a position in a list, so the next page is "everything after this row" no matter what happened in front of it. Rows added before your cursor are simply not in your walk, which is the correct answer for a snapshot.

Ordering and ties

Appointments are ordered by start time, most other resources by creation time. The cursor carries the row's id alongside its sort value, and the ordering is on the pair.

That matters more than it sounds. Timestamps collide constantly here: a recurring series is written in one statement, an import creates a hundred customers inside the same millisecond, and two staff members have appointments starting at the same time by design. On the timestamp alone, rows sharing a value have no defined order between pages and can be served twice or skipped depending on the query plan. On the pair there is exactly one row after any given row.

You do not have to do anything about this. It is here so you know the walk is complete.

Filtering while you page

Filters are ordinary query parameters and must be repeated on every request in the walk, including the ones carrying a cursor. The loop above does that by keeping params outside the iteration.

updatedSince is the one to reach for when you are syncing rather than exporting: pass the timestamp of your last successful sync and page through only what changed.

On this page

  • Walking a list
  • Cursors are opaque
  • Why there is no page number
  • Ordering and ties
  • Filtering while you page
StippaStippa

Appointment scheduling, without the hassle.

For whom

  • For salons
  • Beauty salons
  • Physiotherapists
  • Coaches
  • Personal trainers

Features

  • Booking widget
  • Online payments
  • Reminders
  • No-show prevention
  • Online calendar
  • Client management

Product

  • Features
  • Pricing
  • FAQ
  • Contact
  • System status

Legal

  • Privacy policy
  • Terms of service
  • Data processing agreement

© 2026 Stippa. All rights reserved.

De Rechter Software · Molenwater 20, 4511 BN Breskens · KvK 98466402 · btw NL005332100B80

······