HyphaBase

Adding and changing customers

The four required fields, why phone is one of them, and what to do about the same person using two of your sites.

The four required fields

FieldRule
first_nameRequired, up to 255 characters
last_nameRequired, up to 255 characters
emailRequired, a valid address, unique within the site
phoneRequired, up to 255 characters, stored as sent

Two of those catch people out.

phone is required. There's no way to add a customer without one. If you're importing from a system where phone was optional, decide on a placeholder before you start rather than halfway through a failing import.

phone isn't validated as a phone number. Any string goes in, so no phone is accepted. If you'll ever text these people, normalise to a real format on your side — nothing downstream will do it for you.

Emails are unique per site, not globally

By default Dana can exist twice: once at Portland, once at Salem, as two separate records with their own notes and history.

That's deliberate. Two sites genuinely have separate relationships with the same person, and Portland's staff notes shouldn't appear on Salem's screen.

It also means email isn't a global identifier. Every read is already scoped to one site, so "find Dana across the business" is several requests, not one.

If you'd rather have one shared record across sites, say so when you create them:

{
  "first_name": "Dana",
  "last_name": "Whitfield",
  "email": "dana@example.com",
  "phone": "+15035550143",
  "location_ids": [
    "019aff48-a982-70b4-9be8-57eb265122d7",
    "019aff52-1b03-7c91-8ad2-9e7f3c1b4a08"
  ]
}

Every id has to be a site your credential can reach, or the whole request is a 422. Leave the field out and they belong to the site you created them at.

A shared record has one id everywhere, and an edit at any site is copied to the others a moment later. See Sharing a record across sites for editing, adding and removing sites.

Which to choose: shared records suit a business where customers genuinely move between sites — a chain of clinics, say. Separate records suit sites that operate independently. Sarah keeps them separate, because a Portland customer booking Salem is rare enough to handle by hand.

PATCH or PUT

Both work, and they differ in a way that'll bite you exactly once:

// PATCH — send only what changed. Use this.
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' }),
});

Use PATCH by default. PUT earns its place when you're the system of record and want the API to end up matching your object exactly — then a missing field should be an error rather than a silent no-op.

What comes back after a write

The customer, so you don't need a follow-up read.

Two things to know. name is computed from the first and last name and is read-only — sending it does nothing. And contact fields in the response are masked by the same rules as reads, so a key with clients.create but not clients.view_email will successfully create someone and get a starred-out email back. Confusing the first time; not a failure.

Handling a rejection

const response = await createCustomer(payload);

if (response.status === 422) {
  const { errors } = await response.json();
  // { email: ["The email has already been taken."] }
  for (const [field, messages] of Object.entries(errors)) {
    setFieldError(field, messages[0]);
  }
  return;
}

The keys match the field names you submitted, so binding straight to form fields works.

Next

On this page