Customers & CRM
Admin endpoints for the customer directory, profiles, notes, tags, and tag management.
Customers & CRM
Admin CRM endpoints covering the customer directory, individual profiles,
free-text notes, and the customer-tag system. Every endpoint calls
requireRole(...), which resolves the Better Auth session
(UNAUTHENTICATED 401 when there is none) and checks the caller's role against
the hierarchy customer < staff < receptionist < manager < owner < developer
(FORBIDDEN 403 when the level is too low).
Base URL: admin.theroyalglow.in · Auth: Better Auth session. These
paths carry no /admin prefix — the admin subdomain is the namespace, so
the full URL is e.g. admin.theroyalglow.in/api/customers. Most
endpoints require receptionist; profile overrides
(PATCH /api/customers/[id]) and tag creation (POST /api/tags)
require manager. Money is an integer in paise; dateOfBirth is
YYYY-MM-DD; timestamps are ISO-8601 UTC. A "customer" id is the underlying
user id.
GET /api/customers
Paginated, searchable, sortable customer directory. Returns user rows with
role customer that have a customer_profile, each enriched with KPIs, the
loyalty gems balance (LEFT JOined — null when no account exists), and tag
chips. This endpoint paginates and includes meta.
Minimum role: receptionist (requireRole('receptionist'))
GET /api/customers?q=aarav&sort=ltv&page=1&pageSize=20&tag=vipQuery parameters
Validated by customerListQuerySchema (@rgss/types).
Prop
Type
Response
meta carries pagination. totalPages is max(1, ceil(totalCount / pageSize)).
{
"success": true,
"data": {
"customers": [
{
"id": "usr_abc123",
"name": "Aarav Sharma",
"email": "[email protected]",
"phone": "+919812345678",
"totalVisits": 12,
"totalSpentPaise": 4560000,
"noshowCount": 0,
"firstVisitAt": "2025-09-10T05:30:00.000Z",
"lastVisitAt": "2026-05-28T09:15:00.000Z",
"gemsBalance": 320,
"createdAt": "2025-09-01T04:00:00.000Z",
"tags": [
{ "slug": "vip", "name": "VIP", "color": "#d4af37" }
]
}
]
},
"meta": {
"page": 1,
"totalPages": 3,
"totalCount": 48
}
}Errors
GET /api/customers/[id]
Returns a single customer profile: user identity plus customer_profile KPIs,
the gems balance (null when no loyalty account), and tag chips.
Minimum role: receptionist (requireRole('receptionist'))
GET /api/customers/usr_abc123Path parameters
Prop
Type
Response
{
"success": true,
"data": {
"customer": {
"id": "usr_abc123",
"name": "Aarav Sharma",
"email": "[email protected]",
"role": "customer",
"image": "https://lh3.googleusercontent.com/a/avatar.jpg",
"phone": "+919812345678",
"gender": "male",
"dateOfBirth": "1994-03-22",
"totalVisits": 12,
"totalSpentPaise": 4560000,
"noshowCount": 0,
"lateCancellationCount": 1,
"bookingRequiresApproval": false,
"firstVisitAt": "2025-09-10T05:30:00.000Z",
"lastVisitAt": "2026-05-28T09:15:00.000Z",
"gemsBalance": 320,
"createdAt": "2025-09-01T04:00:00.000Z",
"tags": [
{ "slug": "vip", "name": "VIP", "color": "#d4af37" }
]
}
}
}Errors
PATCH /api/customers/[id]
Manager-level override of profile gates — e.g. reset the no-show count or toggle the booking-approval requirement. Both fields are optional; an empty body is a no-op that returns the current profile.
Minimum role: manager (requireRole('manager'))
PATCH /api/customers/usr_abc123
Content-Type: application/jsonPath parameters
Prop
Type
Request body
Validated by an inline Zod schema in the handler.
Prop
Type
{
"noshowCount": 0,
"bookingRequiresApproval": false
}Response
Returns the updated customer_profile row.
{
"success": true,
"data": {
"customer": {
"id": "cp_7g2k9",
"userId": "usr_abc123",
"phone": "+919812345678",
"gender": "male",
"dateOfBirth": "1994-03-22",
"marketingConsent": true,
"marketingConsentAt": "2025-09-01T04:05:00.000Z",
"appointmentRemindersEnabled": true,
"membershipAlertsEnabled": true,
"acquisitionSource": "gmb",
"utmCampaign": null,
"utmMedium": null,
"utmSource": "gmb",
"firstVisitAt": "2025-09-10T05:30:00.000Z",
"lastVisitAt": "2026-05-28T09:15:00.000Z",
"totalVisits": 12,
"totalSpentPaise": 4560000,
"noshowCount": 0,
"lateCancellationCount": 1,
"consecutiveCompletedBookings": 3,
"bookingRequiresApproval": false,
"createdAt": "2025-09-01T04:00:00.000Z",
"updatedAt": "2026-05-30T10:00:00.000Z"
}
}
}Errors
POST /api/customers/[id]/notes
Adds a free-text note to a customer, optionally linked to a booking. The author (the signed-in user) and a timestamp are persisted automatically.
Minimum role: receptionist (requireRole('receptionist'))
POST /api/customers/usr_abc123/notes
Content-Type: application/jsonPath parameters
Prop
Type
Request body
Validated by addCustomerNoteSchema (@rgss/types).
Prop
Type
{
"content": "Prefers Priya as stylist. Allergic to ammonia-based dyes.",
"bookingId": "bk_8f2a1c9e4d"
}Response
Returns 201 Created with the created note row.
{
"success": true,
"data": {
"note": {
"id": "note_5h3j2",
"customerId": "usr_abc123",
"authorId": "usr_staff09",
"bookingId": "bk_8f2a1c9e4d",
"content": "Prefers Priya as stylist. Allergic to ammonia-based dyes.",
"createdAt": "2026-05-30T10:15:00.000Z"
}
}
}Errors
POST /api/customers/[id]/tags
Assigns an existing tag to a customer. Idempotent: re-assigning a tag the customer already has is a silent no-op (the composite primary key absorbs the conflict). The assigning user is recorded.
Minimum role: receptionist (requireRole('receptionist'))
POST /api/customers/usr_abc123/tags
Content-Type: application/jsonPath parameters
Prop
Type
Request body
Validated by assignTagSchema (@rgss/types).
Prop
Type
{
"tagId": "tag_vip01"
}Response
Returns 201 Created.
{
"success": true,
"data": {
"ok": true
}
}Errors
DELETE /api/customers/[id]/tags/[tagId]
Removes a tag assignment from a customer. No-op if the assignment does not exist (still returns success).
Minimum role: receptionist (requireRole('receptionist'))
DELETE /api/customers/usr_abc123/tags/tag_vip01Path parameters
Prop
Type
Response
{
"success": true,
"data": {
"ok": true
}
}Errors
GET /api/tags
Lists all customer tags, alphabetical by name — used to populate the tag picker.
Minimum role: receptionist (requireRole('receptionist'))
GET /api/tagsResponse
{
"success": true,
"data": {
"tags": [
{
"id": "tag_noshow01",
"name": "No-Show Risk",
"slug": "no-show-risk",
"color": "#e11d48",
"description": null,
"createdAt": "2025-08-15T04:00:00.000Z"
},
{
"id": "tag_vip01",
"name": "VIP",
"slug": "vip",
"color": "#d4af37",
"description": null,
"createdAt": "2025-08-15T04:00:00.000Z"
}
]
}
}Errors
POST /api/tags
Creates a new customer tag. The slug is derived from name in the query layer
(lowercased, whitespace runs collapsed to hyphens).
Minimum role: manager (requireRole('manager'))
POST /api/tags
Content-Type: application/jsonRequest body
Validated by createTagSchema (@rgss/types).
Prop
Type
{
"name": "VIP",
"color": "#d4af37"
}Response
Returns 201 Created with the created tag row.
{
"success": true,
"data": {
"tag": {
"id": "tag_vip01",
"name": "VIP",
"slug": "vip",
"color": "#d4af37",
"description": null,
"createdAt": "2026-05-30T10:20:00.000Z"
}
}
}Errors
Was this page helpful?