Admin Customer Management
Create, search, and manage customer records from the admin API.
For general conventions, see JSON-API Conventions. For authentication, see Authentication.
Prerequisites
- A valid Bearer token with admin access
- Organization context (
?o={organization_id})
Customer vs. Customer Account
The platform distinguishes between two entities:
| Entity | Scope | Purpose |
|---|---|---|
| Customer | Per-organization | A business record with contact info, bookings, and custom fields. Owned by a specific organization. |
| Customer Account | Global (platform-wide) | An end-user identity that can log in. Linked to Customer records across multiple organizations via account_id. |
A single person may have multiple Customer records (one per organization they interact with), all linked to the same CustomerAccount.
Creating a Customer
POST /api/v1/customers?o={org_id}
{
"data": {
"type": "customers",
"attributes": {
"given_name": "Jane",
"family_name": "Doe",
"email": "jane@example.com",
"mobile": "+49 170 1234567",
"company": "Acme Corp",
"locale": "de-DE"
}
}
}
Key Attributes
| Attribute | Type | Description |
|---|---|---|
given_name |
string | First name |
family_name |
string | Last name |
email |
string | Email address |
mobile |
string | Mobile phone number |
phone |
string | Landline phone number |
company |
string | Company name |
sex |
string | Gender |
birth_date |
string | Date of birth (Y-m-d) |
locale |
string | Preferred language (defaults to organization locale) |
Custom Fields
Organizations can define custom fields (type custom-fields) to collect additional data on customer records — member numbers, preferences, notes, etc. Each field value is stored as a custom-entry linked to the customer.
Reading Custom Field Data
There are two ways to retrieve custom field values on a customer.
Option 1 — custom_entry_map attribute (recommended for display)
This attribute is always returned on the customer resource. It is a map keyed by the custom field's UUID:
GET /api/v1/customers/{customer_id}?o={org_id}
{
"data": {
"type": "customers",
"id": "{customer_uuid}",
"attributes": {
"given_name": "Jane",
"custom_entry_map": {
"{field_uuid_1}": {
"value": "Gold",
"label": "Membership Tier",
"formatted_value": "Gold",
"type": "select"
},
"{field_uuid_2}": {
"value": "12345",
"label": "Member Number",
"formatted_value": "12345",
"type": "text"
}
}
}
}
}
| Key | Type | Description |
|---|---|---|
value |
varies | The raw stored value (see field type table below) |
label |
string | Human-readable field name |
formatted_value |
string | Display-ready value (e.g. option labels for select fields) |
type |
string | Field type (text, textarea, number, select, multiselect, checkbox, date) |
Option 2 — include=custom_entries,custom_entries.field (full JSON
Use this when you need the full custom-entries and custom-fields resource objects:
GET /api/v1/customers/{customer_id}?o={org_id}&include=custom_entries,custom_entries.field
Returns custom_entries as a JSON
has-many relationship in the response, with field sideloaded in included.
Writing Custom Field Data
Send the custom_entry_map attribute inside a PATCH request on the customer. The backend performs an atomic upsert and delete: entries in the map are created or updated, and any entries for fields not present in the map are deleted.
PATCH /api/v1/customers/{customer_id}?o={org_id}
{
"data": {
"type": "customers",
"id": "{customer_uuid}",
"attributes": {
"custom_entry_map": {
"{field_uuid_1}": { "value": "Gold" },
"{field_uuid_2}": { "value": "12345" }
}
}
}
}
To clear a specific field, omit it from the map (alongside all fields you want to keep). To clear all custom fields, send an empty object:
"custom_entry_map": {}
Note: Always include all fields you want to retain in a single request. Any field UUID not present in the map will have its entry deleted.
Value Format by Field Type
| Field type | Expected value format |
Example |
|---|---|---|
text |
string |
"Jane" |
textarea |
string |
"Long text..." |
number |
number or numeric string |
42 |
select |
string (option key or label) |
"gold" |
multiselect |
string[] (array of option keys) |
["yoga", "pilates"] |
checkbox |
boolean or "1"/"0" |
true |
date |
string in Y-m-d format |
"1990-06-15" |
File upload fields (file, image) are managed through a separate upload flow and cannot be set via custom_entry_map.
Full Example: Create Customer with Custom Fields
POST /api/v1/customers?o={org_id}
{
"data": {
"type": "customers",
"attributes": {
"given_name": "Jane",
"family_name": "Doe",
"email": "jane@example.com",
"custom_entry_map": {
"{field_uuid_membership_tier}": { "value": "Gold" },
"{field_uuid_member_number}": { "value": "12345" }
}
}
}
}
Then read back the values:
GET /api/v1/customers/{customer_uuid}?o={org_id}
The custom_entry_map in the response will contain the stored values with their labels and formatted representations.
Finding Customers
By Email (Exact Match)
GET /api/v1/customers?o={org_id}&filter[email]=jane@example.com
By Search (Fuzzy)
Searches across name, email, and phone:
GET /api/v1/customers?o={org_id}&filter[search]=jane
By Custom Field
See the custom field filter syntax in JSON-API Conventions.
Resolving a Customer by Code
Look up a customer from a QR code, barcode, or account pass:
GET /api/v1/customers/resolve-code?o={org_id}&code={identifier_code}
Resolves the code via the ModelIdentifier system and returns the associated customer resource.
Creating Custom Model Identifiers
To assign a custom identifier (barcode, member number, etc.) to a customer, create a model-identifier linked to the customer:
POST /api/v1/model-identifiers?o={org_id}
{
"data": {
"type": "model-identifiers",
"attributes": {
"code": "MEMBER-12345",
"type": "custom"
},
"relationships": {
"identifiable": {
"data": {
"type": "customers",
"id": "{customer_uuid}"
}
}
}
}
}
Once created, the customer can be looked up using resolve-code with the assigned code. You can also view a customer's existing identifiers via the model-identifiers relationship:
GET /api/v1/customers/{customer_id}/model-identifiers?o={org_id}
Merging Duplicate Customers
Combine two customer records into one:
POST /api/v1/customers/{source_customer_id}/merge?o={org_id}
{
"referenceId": "{target_customer_uuid}"
}
The merge process:
- Fills empty fields on the target customer from the source (name, phone, birth date, etc.)
- Merges custom field data (target values take priority on conflicts)
- Transfers all relationships (bookings, orders, invoices) to the target
- Links the global account if the source had one and the target doesn't
- Deletes the source customer record
Warning: This operation is irreversible. The source customer is permanently deleted.
Removing a Customer from a Community
GET /api/v1/customers/{customer_id}/remove-from-community?o={org_id}&community_id={community_uuid}
Removes the customer's membership from the specified community.
Exporting Customers
Trigger an asynchronous export:
POST /api/v1/exports?o={org_id}
{
"data": {
"type": "exports",
"attributes": {
"type": "customers",
"filters": {
"search": "jane"
}
}
}
}
The export is processed in the background. Retrieve the download URL from the export resource once complete.
Common Errors
| Error | Cause | Solution |
|---|---|---|
422 — email already exists |
Duplicate email in organization | Use search to find the existing record, or merge |
404 — code not found |
Invalid identifier code | Verify the code belongs to this organization |
422 — merge reference not found |
Invalid target customer UUID | Verify the target customer exists |
Delivery Address
Customers can have a delivery address stored as a separate address resource. This is used when a service or plan requires a delivery address during checkout (configured via requires_delivery_address on the service/plan).
Reading the Delivery Address
Include the delivery_address relationship:
GET /api/v1/customers/{customer_uuid}?o={org_id}&include=delivery_address
The response includes the address in the included array:
{
"data": {
"type": "customers",
"id": "{uuid}",
"relationships": {
"delivery_address": {
"data": { "type": "addresses", "id": "{address_uuid}" }
}
}
},
"included": [
{
"type": "addresses",
"id": "{address_uuid}",
"attributes": {
"street_address": "123 Main St",
"city": "Berlin",
"zip_code": "10115",
"country_code": "DE"
}
}
]
}
How Delivery Addresses Are Set
Delivery addresses are collected during checkout when the booked service has requires_delivery_address: true. The address is submitted as part of the order form data and stored on the customer record. The order also snapshots the delivery address text at the time of purchase.
Including on Other Resources
GET /api/v1/orders?o={org_id}&include=customer.delivery_address
GET /api/v1/bookings/{id}?o={org_id}&include=customer.delivery_address
GET /api/v1/plan-subscriptions?o={org_id}&include=customer.delivery_address