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

Appointments

The core resource: reading the diary, creating a booking, and moving one through its lifecycle.

Last updated 1 August 2026

An appointment is one scheduled slot: a customer, a service, a start time, and usually a staff member. It is the resource everything else on this API exists to support, and the only one you can both read and write.

MethodEndpointScopePlan
GET/api/v1/appointments

List appointments

Query: limitcursorstatusfromtocustomerIdstaffMemberIdserviceIdupdatedSince

appointments:readGroei
POST/api/v1/appointments

Create an appointment

Requires an Idempotency-Key

appointments:writePro
GET/api/v1/appointments/{id}

Retrieve an appointment

appointments:readGroei
PATCH/api/v1/appointments/{id}

Update an appointment

appointments:writePro
POST/api/v1/appointments/{id}/status

Change an appointment status

appointments:writePro

What you get back

{
  "id": "98bb2277-c304-4b27-a63f-aa18fce6173a",
  "status": "confirmed",
  "startTime": "2026-08-11T11:31:00.000Z",
  "endTime": "2026-08-11T11:46:00.000Z",
  "notes": null,
  "bookingReference": null,
  "createdAt": "2026-07-29T17:57:46.643Z",
  "updatedAt": "2026-07-29T17:57:46.643Z",
  "customer": {
    "id": "75af1e75-0222-4560-a39f-02424c207f2b",
    "firstName": "Christiaan",
    "lastName": "Veenstra",
    "email": "nola16-693@example.nl",
    "phone": "+3160000694"
  },
  "service": {
    "id": "b50661da-7649-4c14-b748-91bbee8e9643",
    "name": "Baard trimmen",
    "durationMinutes": 15,
    "priceCents": 1200
  },
  "staffMember": { "id": "db8914de-a84e-49f9-8625-f44b4abcf4d6", "name": "Sanne Jansen" },
  "basePriceCents": null,
  "totalPriceCents": null,
  "depositCents": null,
  "currency": "EUR",
  "recurringSeriesId": null,
  "isRecurringOccurrence": false,
  "segments": [
    {
      "id": "0a57c90f-d295-45b1-8d43-f4d61568e69e",
      "position": 0,
      "kind": "active",
      "startTime": "2026-08-11T11:31:00.000Z",
      "endTime": "2026-08-11T11:46:00.000Z",
      "staffMemberId": "db8914de-a84e-49f9-8625-f44b4abcf4d6",
      "label": null
    }
  ]
}

The customer, service and staff member are always embedded, so listing a day's diary is one request rather than one plus three lookups per row. They are deliberately smaller than the standalone resources: enough to render and to contact somebody, not the whole record.

Every field is documented with its type in the OpenAPI document. This page covers what the types do not say.

Filtering the list

ParameterFilters on
statusExact match on one status
from, toStart time, not creation time. Both inclusive, both ISO 8601
customerId, staffMemberId, serviceIdExact match. Combine freely; they intersect
updatedSinceLast modification. The one to sync on

from and to filtering on start time is the choice worth knowing, because the other reading is plausible and wrong for the job. Asking "what is on for next week" is a question about when the appointments happen. Asking "what changed since my last sync" is a question about modification time, and that is updatedSince, which catches a booking made months ago and cancelled this morning. from and to never would.

Sorting is fixed: startTime descending, then id descending. There is no sort parameter in v1.

Creating one

POST /api/v1/appointments goes through the same service the public booking widget uses. The overlap pre-check, the customer deduplication, the price snapshot, the confirmation email and the appointment.created event all happen exactly as they do for a widget booking. There is no back door that skips them.

curl -X POST https://stippa.nl/api/v1/appointments \
  -H "Authorization: Bearer $STIPPA_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "serviceId": "b50661da-7649-4c14-b748-91bbee8e9643",
    "startTime": "2026-08-20T09:00:00.000Z",
    "customer": {
      "firstName": "Renee",
      "lastName": "de Vries",
      "email": "renee@voorbeeld.nl",
      "phone": "0612345678"
    }
  }'

Three things about the body:

There is no endTime. The footprint is derived from the service, every time, including when a PATCH changes the start time or the service. A client that computed an end time would be computing it from a durationMinutes that can change between your read and your write.

customer is an object, not an id. Pass the person's details and the API deduplicates by email within the business: an existing customer is reused, a new one is created. That is the same rule the widget follows, and it is why a booking flow does not need customers:write at all. If you already hold a customer id, you still pass the email; the id is not accepted here.

staffMemberId is optional. Omit it and the business's own assignment rules pick somebody. Pass staffLocked: true alongside it when the customer specifically asked for that person, so a later reschedule does not quietly reassign them.

⚠️

Idempotency-Key is required on this endpoint. A create without one is a 400. See idempotency for why, and for how to pick a key that actually protects you.

When the business takes prepayment

The 201 body is not bare an appointment. It is:

{
  "appointment": { "...": "as above, with status pending_payment" },
  "requiresPayment": true,
  "payment": {
    "amountCents": 1200,
    "currency": "EUR",
    "provider": "mollie",
    "amountKind": "full",
    "checkoutUrl": "https://..."
  }
}

requiresPayment is false and payment is absent for most businesses. When it is true, the appointment exists but is not confirmed, and the customer has to settle it: send them to checkoutUrl on a Mollie business, or use clientSecret with Stripe Elements on a Stripe one. Exactly one of the two is present. Nothing else about your integration changes.

The rules a create can fail

The business configures rules the widget enforces, and this endpoint enforces the same ones. Each refusal is a 422 carrying the rule's own code, listed on the errors page.

If you are creating on the business's behalf rather than a customer's, for example from their own back-office tool or a phone booking, send enforceBookingRules: false and none of them apply. A receptionist taking a booking by phone is not subject to the rules the public widget is. Leave it at its default in every other case.

A slot that is already taken is a 409, from a pre-check before the insert and from a database constraint behind it. Both exist deliberately: the check gives you a clean error, and the constraint means a race cannot double-book anybody even if the check passes twice at once.

Changing one

PATCH /api/v1/appointments/{id} takes serviceId, staffMemberId, startTime and notes, and takes no Idempotency-Key, because a PATCH is naturally idempotent.

status is not a field on the PATCH. It has its own endpoint, and the split is a real distinction rather than tidiness: POST /api/v1/appointments/{id}/status sends notifications and fires webhooks, and a field edit does not. Renaming a note should not email the customer.

curl -X POST https://stippa.nl/api/v1/appointments/98bb2277-c304-4b27-a63f-aa18fce6173a/status \
  -H "Authorization: Bearer $STIPPA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "completed" }'

Four destinations are offered: confirmed, cancelled, completed and no_show. The others are lifecycle states the product moves rows into rather than places you send one. A transition the appointment cannot make is a 422 INVALID_STATUS_TRANSITION. Reinstating a cancelled or no-show appointment re-checks the slot, so it can also be a 409 if somebody else has taken the time in the meantime.

ℹ️

A confirmation really does send mail. Transitioning an appointment on a live business notifies that customer, so do your first experiments on an appointment you created yourself.

Two shapes that surprise people

The segments array is the real block layout

Most appointments have one segment covering the whole booking, and you can ignore the array. Some have several, and then the appointment is not a solid block of time.

A colour treatment is the standard case: apply, wait thirty minutes, then cut. The waiting time is a gap segment, the staff member is free during it, and the business books somebody else into it. startTime and endTime on the appointment still span the whole thing, so a calendar drawn from those two fields is correct about when the customer arrives and leaves, and wrong about when the chair is occupied. If you are mirroring the diary rather than reading it, draw the segments.

Recurring series

recurringSeriesId names the series a row belongs to and isRecurringOccurrence says whether it is one. Both are read-only here: this API creates one-off appointments, and a series is configured in the product.

Only materialized occurrences appear in the list. A weekly series that has been expanded eight weeks ahead returns eight rows; the ninth week exists as a rule rather than as an appointment and is not in any response. So a full read is not a complete forecast of the diary, and there is no endpoint that makes it one. Fetching a series template id directly is a 404, because a template is a rule and not an appointment.

On this page

  • What you get back
  • Filtering the list
  • Creating one
  • When the business takes prepayment
  • The rules a create can fail
  • Changing one
  • Two shapes that surprise people
  • The segments array is the real block layout
  • Recurring series
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

······