Services
What you offer and what it costs. Defined once, optionally shared across every site.
Brightside offers a standard clean, a deep clean, and an end-of-tenancy clean. The deep clean is £120 for a three-bed and takes three hours. Customers can add oven cleaning for £25.
That's this module. Services are what you sell, with a price and optional tax treatment. Service groups organise them for display. Add-ons are the extras someone ticks on top.
Read it instead
Services is what you sell and what it costs, defined once and offered at whichever sites you choose. You reach it over REST: a secret key and a location header.
A service carries a name, a price, whether it is taxable, and a tax category. Groups set display order on a booking page, add-ons hang off it like oven cleaning at twenty-five pounds, and location_ids offer the same clean at Portland and Salem.
Grant services.view on a read-only key and your public price list comes straight from the API. Subscribe to services.service.price_changed and a rate rise reaches the booking page the moment it is saved.
Four scopes decide what a key can do. One that only reads can never move a number, which is what makes a public price list safe to ship.
Services: name, price, groups and add-ons on one record, shared across sites and priced in one place. The guide is below.
Before you start
- The module switched on for the site. If it isn't, everything below returns
403regardless of your key — check the site's modules on the Locations page first. - A credential with the right scopes —
services.viewto read,services.createto add. See what to grant. - The site's id, for
X-Location-Idon every request.
What a service looks like
Prop
Type
The endpoints
The six service endpoints below are stable. Groups and add-ons are in beta — they work, but their shape may still change, so pin to a dated API version before building on them.
| For | Endpoint |
|---|---|
| List services | GET /api/modules/services |
| Add one | POST /api/modules/services |
| Get one | GET /api/modules/services/{service} |
| Change some fields | PATCH /api/modules/services/{service} |
| Replace all fields | PUT /api/modules/services/{service} |
| Delete | DELETE /api/modules/services/{service} |
| List groups | GET /api/modules/services/service-groups |
| Add a group | POST /api/modules/services/service-groups |
| Change a group | PATCH /api/modules/services/service-groups/{serviceGroup} |
| Delete a group | DELETE /api/modules/services/service-groups/{serviceGroup} |
| A service's add-ons | GET /api/modules/services/{service}/addons |
| Add an add-on | POST /api/modules/services/{service}/addons |
| Change an add-on | PATCH /api/modules/services/{service}/addons/{addon} |
| Delete an add-on | DELETE /api/modules/services/{service}/addons/{addon} |
Adding a service
await fetch('https://api.hyphabase.com/api/modules/services', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': readCookie('XSRF-TOKEN'),
'X-Location-Id': locationId,
},
body: JSON.stringify({
name: 'Deep clean — 3 bed',
description: 'Three hours, two cleaners, inside cupboards and appliances.',
price: 120,
taxable: true,
}),
});Only name and price are required. Everything else is optional, which means a service
created with two fields is valid and sellable. It takes 60 minutes unless you say otherwise.
A successful create returns 201 with the new service. A negative or non-numeric
duration_minutes, buffer_time_minutes or capacity_cost returns 422.
Changing and deleting. PATCH changes only the fields you send; PUT replaces the whole record. If two people
edit the same service, the last write wins.
DELETE archives the service: it disappears from the list and from GET, which then
returns 404, but invoices and bookings that used it keep their history.
Offering it at more than one site
Pass location_ids with every site that should offer it:
{
"name": "Deep clean — 3 bed",
"price": 120,
"location_ids": ["019aff48-…", "019aff52-…"]
}Leave it out and the service belongs to the site you created it at. Portland's price list is genuinely separate from Salem's — which is usually what you want, because a Salem three-bed doesn't cost the same as a Portland one.
Same service everywhere? Define it once at the app level.
If your prices really are identical across sites, maintaining them per site is a chore that drifts. Standard services can be defined once for the whole application and pushed down to the sites that opt in, so an edit happens in one place. That's set up in the dashboard rather than through the API — see Model your business.
Groups, for display
A group is a heading on your booking page. "Regular cleans", "One-off deep cleans", "Move-out".
curl -X POST https://api.hyphabase.com/api/modules/services/service-groups \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-H "Content-Type: application/json" \
-d '{"name": "One-off deep cleans", "description": "Booked as needed."}'Then set service_group_id on the services that belong to it. Grouping is presentational
— nothing about pricing or booking depends on it.
Add-ons
The extras a customer ticks alongside the main thing. Oven cleaning, inside windows, fridge.
Prop
Type
curl -X POST https://api.hyphabase.com/api/modules/services/$SERVICE_ID/addons \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-H "Content-Type: application/json" \
-d '{"name": "Oven clean", "price": 25, "display_order": 1}'Add-ons belong to one service, so "oven clean" on both the standard and deep clean is two records. If you find yourself maintaining the same add-on across many services, that's a sign it should be a service in its own right.
What you get told about
| Event | When |
|---|---|
services.service.created | A service is added |
services.service.price_changed | Its price changes |
price_changed is the one worth subscribing to. If you cache prices anywhere — a booking
page, a mobile app — this is how you know to refresh rather than quoting last month's
figure. See Getting told when things happen.
What to grant
| Scope | Lets you |
|---|---|
services.view | Read services, groups and add-ons |
services.create | Add them |
services.update | Change them, including prices |
services.delete | Remove them |
services.view is safe for a publishable key, which is what makes a public price list
possible without a server in between.
Be careful with services.update — it's the scope that can change what you charge. A
booking page never needs it.
What goes wrong
Recipes
Publish a price list without a server. Grant a publishable key services.view, fetch
groups and services from your booking page, and render them. No secret key, no backend.
Raise prices for the new year. List services, PATCH each one's price, and let the
services.service.price_changed event invalidate whatever you've cached. Do it per site —
Portland and Salem are separate lists unless you're sharing at the app level.
Mirror your catalogue into a booking widget. Subscribe to services.service.created
and price_changed, and upsert into your own store. Everything in
When deliveries fail applies.