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
| Field | Rule |
|---|---|
first_name | Required, up to 255 characters |
last_name | Required, up to 255 characters |
email | Required, a valid address, unique within the site |
phone | Required, 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.