Royal Glow internal docs · now fully interactive — Steps, API tables, file trees & live status
Royal Glow Docs
API Reference

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=vip

Query 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_abc123

Path 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/json

Path 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/json

Path 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/json

Path 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_vip01

Path 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/tags

Response

{
  "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/json

Request 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

OpenReport an issue

Was this page helpful?

On this page