A service is something the business sells time for: a haircut, a colour treatment, a consultation. It carries the duration a booking of it occupies and the price it costs, and it is the first thing you read in any booking flow because everything else needs its id.
Read-only over this API, permanently. A service is priced and configured by the business, and a leaked key that could reprice a salon's whole menu is a much worse outcome than one that can read it.
| Method | Endpoint | Scope | Plan |
|---|---|---|---|
| GET | /api/v1/servicesList services Query: | services:read | Groei |
| GET | /api/v1/services/{id}Retrieve a service | services:read | Groei |
| GET | /api/v1/service-categoriesList service categories Query: | services:read | Groei |
What you get back
{
"id": "cb13e0f4-d4fc-406e-9dae-caa0a1d4ab2f",
"name": "Snor + baard",
"description": null,
"durationMinutes": 20,
"priceCents": 1500,
"isActive": true,
"categoryId": "d86bca73-2bdd-43e8-acc8-04be8815fc7f",
"paymentRequired": false,
"depositCents": null,
"imageUrl": null,
"segments": [],
"createdAt": "2026-07-29T17:57:46.236Z",
"updatedAt": "2026-07-29T17:57:46.236Z"
}
Money is in cents, as an integer, always. 1500 is fifteen euros. There are no decimal amounts
anywhere on this API, in either direction, because a price that round-trips through a float is a
price that eventually charges somebody 14.999999 euros. currency on the resources that carry it is
ISO 4217 in upper case.
durationMinutes is what a booking of this service occupies, and it is the reason
POST /api/v1/appointments has no endTime field: the end is derived from this value at write
time, so the two cannot disagree.
Inactive, deleted, and the difference
Three states, and only two of them are visible to you.
Inactive (isActive: false) means the business has taken the service off the menu without
throwing it away. Those rows are in the list by default: isActive is a filter you apply, not one
applied for you, so narrow it yourself when you are building a picker.
# What a customer may book now
curl "https://stippa.nl/api/v1/services?isActive=true&limit=100" \
-H "Authorization: Bearer $STIPPA_KEY"
Deleted services are never returned at all, under any filter, and there is no parameter that
reveals them. The gap this creates is worth knowing about because you cannot see it from a response:
on the business we tested against, the services table held 43 rows and this endpoint returned 20. The
other 23 were deleted, and nothing in the payload says so. hasMore was false and the page was
complete, because "everything" means everything you may see.
That is only a problem if you are reconciling counts against something else. It does not break appointment resolution, because an appointment embeds its service rather than referencing it by id alone: an appointment on a service the business has since deleted still carries that service's name, duration and price in its own payload. Building a service map from this endpoint and looking ids up in it will still miss those, so read the appointment's embedded copy rather than your map.
An inactive service is not bookable, and GET /api/v1/availability/slots returns nothing for one.
Categories are a flat list you resolve yourself
categoryId is not expanded, because a category has no bearing on booking and expanding it on every
row of a hundred-service menu is bytes nobody reads. GET /api/v1/service-categories returns the
whole tree in one page:
{
"id": "fc0f829e-25c2-45ca-aca5-2d4ab92dccdc",
"name": "Verzorging",
"parentId": null,
"position": 3,
"createdAt": "2026-07-29T17:57:46.232Z",
"updatedAt": "2026-07-29T17:57:46.232Z"
}
Fetch it once, build a map, and resolve locally. parentId supports nesting and is usually null;
position is the order the business arranged them in and is what you sort a menu by, not name.
Services, categories and staff change a few times a year. Cache them for as long as your product can tolerate being wrong about a menu, and spend your rate-limit budget on availability instead, which changes constantly. See rate limits.
The detail endpoint returns more than the list
GET /api/v1/services/{id} is a different shape from a row of GET /api/v1/services: it adds the
service's time-based pricing rules, which the collection omits.
That is the one place on this API where the item is not the list row, and it is a size decision. A
business can price a service differently by day and time, so the rules are an array per service, and
carrying them on every row of a list request would make the common case pay for a feature most
businesses do not use. Fetch the detail when you need to quote a price for a specific time; use the
list's priceCents for a menu.
priceCents on the list is the standard price. A booking's real price is snapshotted onto the
appointment when it is created, which is what basePriceCents and totalPriceCents are on the
appointments resource. Do not compute a total from a service price
after the fact: the service may have been repriced since.
Segmented services
segments on a service is the template that produces the segments on an appointment. It is empty
for most services and non-empty for the ones with a processing pause in the middle, like a colour
treatment where the customer waits thirty minutes between two worked periods.
If it is non-empty, durationMinutes covers the whole span including the gap, and the staff member
is genuinely free during that gap. Anything that draws a diary should read the appointment's own
segments rather than assuming a solid block.