HyphaBase

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.

What you can build with Customers · 1:19
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 403 regardless of your key — check the site's modules on the Locations page first.
  • A credential with the right scopes — clients.view to read, clients.create to add. See what to grant.
  • The site's id, for X-Location-Id on 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

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:

ParameterDoes
searchMatches part of a first name, last name, email or phone, ignoring case
client_status_idOnly people with that status at this site
date_from, date_toWhen they were added, inclusive
include_deletedShow deleted people alongside current ones
only_deletedOnly deleted people — a recycle bin
page, per_pagePaging. 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:

  • PATCH changes only what you send. This is what you want almost always.
  • PUT replaces everything. Send the whole record — a partial PUT fails 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

What you get told about

EventWhen
clients.client.createdSomeone's added
clients.client.updatedAny field changes
clients.client.deletedSomeone's deleted
clients.note.addedA 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

ScopeLets you
clients.viewRead customers and everything attached to them
clients.createAdd customers
clients.updateChange customers
clients.deleteDelete, undelete, and delete for good
clients.view_nameSee real names rather than starred-out ones
clients.view_emailSee real email addresses
clients.view_phoneSee 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

Next

For every field, parameter and response shape, the reference has the lot: Clients.

On this page