Skip to content
For the first ten businesses: six months of Stippa free, in exchange for your feedback.Find out more
Stippa
FeaturesPricingDemoHelpContact
Log inStart free

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 to a spreadsheet
  • 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 27 October 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.

Not every customer has one. A business can keep somebody on file without an email address, from the counter or the dashboard, and then email is null on the response. This API never creates such a row, since email is required on its own creates, but it does return them. Key a mirror on id, not on email.

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,
  "locale": "nl",
  "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"
}

The customer's language

locale is the language the business writes to this customer in: confirmations, reminders and every other customer message. It is one of nl, en, de, fr, es, pl or uk, or null when the language is not known yet, in which case messages go out in the business's own language.

Nobody has to fill it in. The first booking that carries a language sets it, whether that booking came through the widget or through POST /api/v1/appointments with a locale, and a later booking never overwrites a language that is already there. You can also pass it on a create, and change it with a PATCH; sending "locale": null on a PATCH clears it back to unknown.

The same field is in every customer.* webhook payload, because that payload is this resource's serializer output.

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. 2 to 100 characters; a shorter term is a 400
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 the four contact fields, firstName, lastName, email and phone, plus locale, described above. 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
  • The customer's language
  • 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
Stippa

Appointment scheduling, without the hassle.

Try free for 1 month

Product

  • Features
  • Pricing
  • Demo
  • FAQ
  • Contact
  • System status

Features

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

For whom

  • For salons
  • Beauty salons
  • Physiotherapists
  • Coaches
  • Personal trainers
  • Barbershops
  • Nail salons
  • Pedicurists
  • Massage salons

Legal

  • Privacy policy
  • Terms of service
  • Data processing agreement

© 2026 Stippa. All rights reserved.

De Rechter Software · Molenwater 20, 4511 BN Breskens · KvK 98466402· VAT NL005332100B80 · +31 6 15745447

······
Try free for 1 month

1 month free. No card needed.

Cookies

We use cookies to measure how visitors use this website. With your consent we also record your visit, so we can see where something goes wrong.

You can change your choice at any time via 'Cookie preferences' at the bottom of every page.

· Learn more