Customers
Who your customers are, where they live, what they like, and whether they're still coming back.
Brightside Cleaning has about 300 regulars. Dana on Thursday mornings, who has a dog and prefers unscented products. The Hendersons who moved in March. The flat on Division that cancelled in January and hasn't been back.
That's what this module holds. Contact details, addresses, notes for the things staff need to remember, tags for grouping people, and a status for where they are in your world — new, regular, lapsed.
It also does bulk work: import a spreadsheet, export a filtered list, and watch the progress of either.
Read it instead
Customers is the record every other module points at: the people you serve, and what your crew needs to know before they arrive. You reach it over REST — a secret key and a location header.
One record carries contact details, addresses, notes like a key safe code, tags such as has-dog or VIP, and a status per site — new, regular, or lapsed. It all comes back expanded, so one request fills a profile screen.
Filter on client status and the response is a win-back campaign: every lapsed customer at one site. Subscribe to client created and note added to stay in step without a nightly job. Import a spreadsheet whole, rather than looping over three hundred people.
Contact details are permissioned field by field: a key with clients.view but not clients.view_email counts customers and still sees every address starred out.
Customers: contact details, addresses, notes and tags on one record — permissioned field by field, and kept in step by webhooks. The guide is below.
Before you start
Four things, and three are the same for every module.
- 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 —
clients.viewto read,clients.createto add. See what to grant. - The site's id, for
X-Location-Idon every request. - The base URL —
https://api.hyphabase.com.
If any of that is unfamiliar, do your first hour first.
What a customer looks like
Prop
Type
Contact details can come back starred out
first_name, last_name, email and phone each have their own permission —
clients.view_name, clients.view_email, clients.view_phone. A credential with
clients.view but not clients.view_email gets the first three characters and then
asterisks. Not an error, and not null.
So if an email reads dan*****, that's a missing scope rather than corrupt data. It's
also genuinely useful: Sarah's reporting job can count customers per site without
being handed 300 people's personal details.
The endpoints
| For | Endpoint |
|---|---|
| List them | GET /api/modules/clients |
| Add one | POST /api/modules/clients |
| Get one | GET /api/modules/clients/{client} |
| Change some fields | PATCH /api/modules/clients/{client} |
| Replace all fields | PUT /api/modules/clients/{client} |
| Delete (recoverable) | DELETE /api/modules/clients/{id} |
| Undelete | POST /api/modules/clients/{id}/restore |
| Delete for good | DELETE /api/modules/clients/{id}/force |
| Their addresses | GET /api/modules/clients/{client}/addresses |
| Their notes | GET /api/modules/clients/{client}/notes |
| Their tags | GET /api/modules/clients/{client}/traits |
| Their status | GET /api/modules/clients/{client}/statuses |
| Available statuses | GET /api/modules/clients/client-statuses |
| Import a spreadsheet | POST /api/modules/clients/import-export/import |
| Ask for an export | POST /api/modules/clients/import-export/export |
Two routes use a different parameter name
Most take {client}; restore and force take {id}. It's the same id either way —
it only matters if you're building URLs from a template.
Adding a customer
All four contact fields are required, and the email has to be new at this site.
const response = await fetch(
'https://api.hyphabase.com/api/modules/clients',
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': readCookie('XSRF-TOKEN'),
'X-Location-Id': locationId,
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
first_name: 'Dana',
last_name: 'Whitfield',
email: 'dana@example.com',
phone: '+15035550143',
}),
},
);
if (response.status === 422) {
const { errors } = await response.json();
// { email: ["The email has already been taken."] }
return showFieldErrors(errors);
}201 Created:
{
"data": {
"id": "019fb0c2-4d31-7a12-b8e4-1c93a7f20e55",
"first_name": "Dana",
"last_name": "Whitfield",
"name": "Dana Whitfield",
"email": "dana@example.com",
"phone": "+15035550143",
"created_at": "2026-07-30T00:52:11+00:00",
"updated_at": "2026-07-30T00:52:11+00:00"
}
}By default they belong to the site you created them at. Pass location_ids for someone
who uses two of your sites. More in
Adding and changing customers.
Finding customers
Newest first, fifteen at a time. Filters go on the query string:
| Parameter | Does |
|---|---|
search | Matches part of a first name, last name, email or phone, ignoring case |
client_status_id | Only people with that status at this site |
date_from, date_to | When they were added, inclusive |
include_deleted | Show deleted people alongside current ones |
only_deleted | Only deleted people — a recycle bin |
page, per_page | Paging. per_page caps at 100. |
const params = new URLSearchParams({ search: 'whitfield', per_page: '25' });
const response = await fetch(
`https://api.hyphabase.com/api/modules/clients?${params}`,
{ credentials: 'include', headers: { 'X-Location-Id': locationId } },
);
const { data: customers, meta } = await response.json();Drive your paging from meta.current_page and meta.last_page — see
Long lists.
Fetching one person returns their sites, status and tags already expanded, so you usually don't need follow-up requests to build a profile screen.
Changing one
Two verbs, and they behave differently:
PATCHchanges only what you send. This is what you want almost always.PUTreplaces everything. Send the whole record — a partialPUTfails validation rather than quietly keeping the old values.
await fetch(`${BASE}/api/modules/clients/${customerId}`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': readCookie('XSRF-TOKEN'),
'X-Location-Id': locationId,
},
body: JSON.stringify({ phone: '+15035550199' }),
});Deleting one
Deleting is reversible by default, which is the right call for customer data — an accidental tap in a busy office shouldn't lose ten years of history.
# Recoverable. Hidden from lists.
curl -X DELETE https://api.hyphabase.com/api/modules/clients/$CUSTOMER_ID \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID"
# Bring them back. Note {id}.
curl -X POST https://api.hyphabase.com/api/modules/clients/$CUSTOMER_ID/restore \
-u sk_live_xxx: -H "X-Location-Id: $HYPHABASE_LOCATION_ID"Full detail, including the version with no undo, in Deleting and undeleting.
What else hangs off a customer
Addresses, notes and tags
Where they live, what staff need to remember, and how to group people.
Statuses
New, regular, lapsed — one per customer, per site.
Moving lots at once
Spreadsheets in, spreadsheets out.
What you get told about
| Event | When |
|---|---|
clients.client.created | Someone's added |
clients.client.updated | Any field changes |
clients.client.deleted | Someone's deleted |
clients.note.added | A note is added to a customer |
data.object is the whole customer. Subscribe under Webhooks in the dashboard, where
the live list of events is shown — trust that over this table. Worked receiver in
Customer events.
What to grant
| Scope | Lets you |
|---|---|
clients.view | Read customers and everything attached to them |
clients.create | Add customers |
clients.update | Change customers |
clients.delete | Delete, undelete, and delete for good |
clients.view_name | See real names rather than starred-out ones |
clients.view_email | See real email addresses |
clients.view_phone | See real phone numbers |
The three view_* scopes stack on top of clients.view. A key with only clients.view
can count and list people but sees masked contact details — a sensible default for
anything analytical.
clients.view is safe for a publishable key, so a booking page can read. Everything that
writes needs a secret key or a signed-in person. See
Who can do what.
What goes wrong
Recipes
Sync from another system
Without creating duplicates on the second run.
Find duplicates
Same person, two records, two email addresses.
Tag everyone at once
A backfill you can safely run twice.
Next
Adding and changing customers
Validation, people at two sites, PUT versus PATCH.
Addresses, notes and tags
Everything else attached to a customer.
For every field, parameter and response shape, the reference has the lot: Clients.