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

Customers

People on the books, deduplicated by email, and the fields this API deliberately will not write.

Last updated 1 August 2026

A customer is a person on the books of one business. Email is the natural key: there is at most one customer per email address per business, and everything about this resource follows from that.

MethodEndpointScopePlan
GET/api/v1/customers

List customers

Query: limitcursoremailsearchupdatedSince

customers:readGroei
POST/api/v1/customers

Create a customer

Requires an Idempotency-Key

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

Retrieve a customer

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

Update a customer

customers:writePro

What you get back

{
  "id": "1fbe8438-e37a-497b-85f3-38d7e168a369",
  "firstName": "Renee",
  "lastName": "de Vries",
  "email": "renee@voorbeeld.nl",
  "phone": "0612345678",
  "marketingOptIn": false,
  "totalAppointments": 12,
  "totalNoShows": 0,
  "totalCancellations": 1,
  "lastBookingAt": "2026-07-14T08:30:00.000Z",
  "anonymizedAt": null,
  "createdAt": "2026-07-29T20:32:31.337Z",
  "updatedAt": "2026-07-29T20:32:31.337Z"
}

You usually do not need customers:write

The most common reason to reach for this resource is to create somebody before booking them, and that is not how it works here. POST /api/v1/appointments takes a customer object and deduplicates on the email itself: an existing person is reused, a new one is created. A booking flow needs appointments:write and no customer scope at all.

Reach for customers:write when the customer record is the thing you are managing rather than a side effect of a booking. Importing a list from a previous system, or syncing a CRM where somebody edits a phone number in your tool and it should land here too.

Creating: read the status code, not just the body

POST /api/v1/customers deduplicates on email too, and the status code is the only thing that tells you which happened:

StatusMeans
201A new customer was inserted
200That email was already on file. The body is the existing record

A duplicate is not an error, because for the ordinary case it is not one: a CRM sync that pushes a hundred people every night is telling you about ninety-nine you already know. A 409 there would make the normal path look like a failure and push every caller into a lookup-then-create dance with a race in the middle.

⚠️

On a 200, the fields you sent were discarded. The response is the stored record, not a merge of it with your request, so a name or phone number that differs from what is on file is silently dropped and the body you get back will not match the body you sent.

This is the one thing that will quietly cost you an import. Branch on the status code: treat 200 as "already exists, decide whether to PATCH it", not as "saved". A loop that posts a CSV and checks only for a 2xx will report a hundred successes and change nothing.

Idempotency-Key is still required. The dedupe protects you against a repeated address; the key protects you against a repeated request, and only the key can tell a retry apart from a genuinely second submission.

Finding somebody

ParameterBehaviour
emailExact match, case-insensitive. Returns zero or one
searchPartial match across name and email, accent-insensitive
updatedSinceLast modification. The one to sync on

email and search are different tools rather than a strict and a loose version of one. email is the identity lookup: you hold an address and you want the record, and one result or none is the answer. search is what you put behind a text box, and it is accent-folded in both directions, so Renee finds Renée and the reverse. Do not use search to resolve an identity: jan@ matches every address starting that way.

An email with no customer is a normal 200 with data: [], never a 404. The list endpoint answers "how many match", and zero is a valid answer.

Fields this API will not write

POST and PATCH take exactly four fields: firstName, lastName, email, phone. Everything else on the response is refused, and each refusal is a decision.

The counters (totalAppointments, totalNoShows, totalCancellations, lastBookingAt) are derived from the appointments table. A writable no-show count is a number that disagrees with the appointments it is supposed to summarise, and the business makes decisions about people from it.

marketingOptIn and the notification preferences are consent. Consent has to be traceable to the moment a person gave it, and an API key cannot testify to that. The customer sets these through their own preference link, or the business records them from a conversation.

Custom fields are not on this API in any form. They are business-defined and typed at runtime, so they cannot be a stable v1 contract.

Deletion has no endpoint and no scope. Deleting a customer erases appointment history and is legally consequential, and it is not something a leaked key should be able to do to a business's whole book. It stays behind a logged-in human. See permissions.

Anonymised customers

anonymizedAt is a timestamp when the person has exercised their right to erasure. The row survives because the appointments referencing it have to, and it stays in list responses for the same reason: omitting it would make a mirrored customer list disagree with a mirrored appointment list.

The identifying fields are overwritten with placeholders rather than nulled, which matters because a null is something your code probably already handles and a plausible-looking wrong name is not:

{
  "firstName": "Geanonimiseerde",
  "lastName": "klant",
  "email": "anonymized+89775ffe-f1c1-45a6-84cd-aae76a766e31@noreply.stippa.app",
  "phone": "",
  "anonymizedAt": "2026-07-30T09:12:44.000Z"
}

Branch on anonymizedAt, not on the shape of the values. Those two Dutch words are the only Dutch in any response on this API and they are not a contract; the timestamp is. Treat a non-null anonymizedAt as "do not contact, do not display": that address is deliverable nowhere, and a PATCH writing real details back onto one is undoing an erasure request somebody made.

On this page

  • What you get back
  • You usually do not need customers:write
  • Creating: read the status code, not just the body
  • Finding somebody
  • Fields this API will not write
  • Anonymised customers
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

······