Memberships
Customer SPA membership view plus the admin endpoints to create, list, inspect, cancel, and record sessions against memberships.
Memberships
SPA memberships are hour-based: a customer buys a tier (e.g. Silver 8hrs, Gold
15hrs) valid for a fixed number of days, and each SPA session deducts its
duration from the remaining hours. There is one customer-facing read endpoint
(theroyalglow.in/api/membership, singular); everything else is an admin
endpoint served from admin.theroyalglow.in (e.g.
admin.theroyalglow.in/api/memberships, plural) and is role-gated.
Base URLs: the customer read endpoint is https://theroyalglow.in; the
admin endpoints are served from https://admin.theroyalglow.in with no
/api/admin/ segment — the subdomain is the namespace (e.g.
admin.theroyalglow.in/api/memberships, .../api/membership-tiers). Roles
are hierarchical: customer < staff <
receptionist < manager < owner < developer. Money is an integer in
paise (₹10,000 = 1000000). Hours are stored as minutes — the
*HoursMinutes fields hold minutes, so Silver 8hrs = 480 and Gold 15hrs =
900. Dates in request bodies are YYYY-MM-DD; timestamps in responses are
UTC ISO strings. Membership status is one of active, expired, cancelled.
Membership numbers use the format RG-MEM-{YY}-{branchNumber}-{5 random}.
GET theroyalglow.in/api/membership
Returns the signed-in customer's own membership data: the single active
membership (if any), an array of past (expired/cancelled) memberships, and the
sessions history for the active membership. Strictly scoped to the
authenticated customer — it never exposes another customer's data. When there is
no active membership, active is null and sessions is an empty array.
Minimum role: customer (requireSession)
GET /api/membershipResponse
{
"success": true,
"data": {
"active": {
"id": "mem_9aZ2bYc31",
"membershipNumber": "RG-MEM-26-1-90872",
"customerId": "usr_abc123",
"tierId": "tier_gold",
"tierNameSnapshot": "Gold",
"totalHoursMinutes": 900,
"usedHoursMinutes": 120,
"pricePaidPaise": 1500000,
"startsAt": "2026-06-01T00:00:00.000Z",
"expiresAt": "2026-08-30T18:29:59.999Z",
"status": "active",
"createdBy": "usr_reception01",
"invoiceId": "inv_3kP9qWx20",
"notes": null,
"createdAt": "2026-06-01T05:30:00.000Z",
"updatedAt": "2026-06-10T07:00:00.000Z"
},
"past": [],
"sessions": [
{
"id": "bk_77aa11bb22",
"bookingNumber": "BK-RS-2606-S-44120-M",
"branchId": "branch_rayasandra",
"customerId": "usr_abc123",
"status": "completed",
"serviceType": "spa",
"bookingDate": "2026-06-10",
"startTime": "15:30:00",
"endTime": "17:30:00",
"totalAmountPaise": 0,
"totalDurationMinutes": 120,
"isMembershipSession": true,
"spaMembershipId": "mem_9aZ2bYc31",
"completedAt": "2026-06-10T07:00:00.000Z",
"createdAt": "2026-06-10T07:00:00.000Z",
"services": [
{
"bookingId": "bk_77aa11bb22",
"serviceId": "svc_aromamassage",
"serviceNameSnapshot": "Aromatherapy Massage",
"durationMinutes": 120,
"staffId": "stf_therapist01",
"staffName": "Anita R.",
"displayOrder": 0
}
]
}
]
}
}Errors
GET admin.theroyalglow.in/api/membership-tiers
Lists active membership tiers, ordered by displayOrder — used to populate the
create-membership form / tier picker. Hours and validity prefill the create
form but remain overridable.
Minimum role: receptionist (requireRole('receptionist'))
GET /api/membership-tiersResponse
{
"success": true,
"data": [
{
"id": "tier_silver",
"name": "Silver",
"slug": "silver",
"description": "8 hours of SPA services, valid 90 days.",
"defaultHoursMinutes": 480,
"defaultPricePaise": 1000000,
"defaultValidityDays": 90,
"isActive": true,
"displayOrder": 0,
"createdAt": "2026-05-01T00:00:00.000Z",
"updatedAt": "2026-05-01T00:00:00.000Z"
},
{
"id": "tier_gold",
"name": "Gold",
"slug": "gold",
"description": "15 hours of SPA services, valid 90 days.",
"defaultHoursMinutes": 900,
"defaultPricePaise": 1500000,
"defaultValidityDays": 90,
"isActive": true,
"displayOrder": 1
}
]
}Errors
GET admin.theroyalglow.in/api/memberships
Lists memberships, newest first, each flattened with the customer name and tier name. Optional query filters narrow by tier id and/or status. The payload is the array itself (no wrapping object).
Minimum role: receptionist (requireRole('receptionist'))
GET /api/memberships?status=active&tier=tier_goldQuery parameters
Prop
Type
Response
{
"success": true,
"data": [
{
"id": "mem_9aZ2bYc31",
"membershipNumber": "RG-MEM-26-1-90872",
"customerId": "usr_abc123",
"tierId": "tier_gold",
"tierNameSnapshot": "Gold",
"totalHoursMinutes": 900,
"usedHoursMinutes": 120,
"pricePaidPaise": 1500000,
"startsAt": "2026-06-01T00:00:00.000Z",
"expiresAt": "2026-08-30T18:29:59.999Z",
"status": "active",
"createdBy": "usr_reception01",
"invoiceId": "inv_3kP9qWx20",
"notes": null,
"createdAt": "2026-06-01T05:30:00.000Z",
"updatedAt": "2026-06-10T07:00:00.000Z",
"customerName": "Priya Sharma",
"tierName": "Gold"
}
]
}Errors
POST admin.theroyalglow.in/api/memberships
Creates a SPA membership and its membership_purchase invoice atomically. The
handler enforces one active membership per customer (a friendly 409 ahead of the
DB partial-unique index), resolves the tier for its name snapshot, computes the
expiry from startDate + validityDays, and splits the GST-inclusive price into
base + GST on the invoice. No gems are earned on purchase. A best-effort expiry
notice job is scheduled but never affects the response.
Minimum role: receptionist (requireRole('receptionist'))
POST /api/memberships
Content-Type: application/jsonRequest body
Validated by createMembershipSchema (@rgss/types).
Prop
Type
{
"customerId": "usr_abc123",
"tierId": "tier_gold",
"hoursMinutes": 900,
"pricePaise": 1500000,
"startDate": "2026-06-01",
"validityDays": 90,
"paymentMethod": "upi",
"notes": "Anniversary gift package."
}Response
Returns 201 Created with the created membership and its purchase invoice.
{
"success": true,
"data": {
"membership": {
"id": "mem_9aZ2bYc31",
"membershipNumber": "RG-MEM-26-1-90872",
"customerId": "usr_abc123",
"tierId": "tier_gold",
"tierNameSnapshot": "Gold",
"totalHoursMinutes": 900,
"usedHoursMinutes": 0,
"pricePaidPaise": 1500000,
"startsAt": "2026-06-01T00:00:00.000Z",
"expiresAt": "2026-08-30T18:29:59.999Z",
"status": "active",
"createdBy": "usr_reception01",
"invoiceId": "inv_3kP9qWx20",
"notes": "Anniversary gift package.",
"createdAt": "2026-06-01T05:30:00.000Z",
"updatedAt": "2026-06-01T05:30:00.000Z"
},
"invoice": {
"id": "inv_3kP9qWx20",
"invoiceNumber": "INV-1-2627-92921",
"branchId": "branch_rayasandra",
"bookingId": null,
"customerId": "usr_abc123",
"subtotalPaise": 1271186,
"taxableValuePaise": 1271186,
"gstAmountPaise": 228814,
"totalAmountPaise": 1500000,
"invoiceType": "membership_purchase",
"paymentMethod": "upi",
"paymentStatus": "paid",
"gemsEarned": 0,
"paidAt": "2026-06-01T05:30:00.000Z"
}
}
}Errors
GET admin.theroyalglow.in/api/memberships/[id]
Returns a single membership enriched with customerName, customerEmail, the
full tier object, and its completed-session history (sessions, each with its
booking_service snapshot rows).
Minimum role: receptionist (requireRole('receptionist'))
GET /api/memberships/mem_9aZ2bYc31Path parameters
Prop
Type
Response
{
"success": true,
"data": {
"id": "mem_9aZ2bYc31",
"membershipNumber": "RG-MEM-26-1-90872",
"customerId": "usr_abc123",
"tierId": "tier_gold",
"tierNameSnapshot": "Gold",
"totalHoursMinutes": 900,
"usedHoursMinutes": 120,
"pricePaidPaise": 1500000,
"startsAt": "2026-06-01T00:00:00.000Z",
"expiresAt": "2026-08-30T18:29:59.999Z",
"status": "active",
"createdBy": "usr_reception01",
"invoiceId": "inv_3kP9qWx20",
"notes": null,
"createdAt": "2026-06-01T05:30:00.000Z",
"updatedAt": "2026-06-10T07:00:00.000Z",
"customerName": "Priya Sharma",
"customerEmail": "[email protected]",
"tier": {
"id": "tier_gold",
"name": "Gold",
"slug": "gold",
"description": "15 hours of SPA services, valid 90 days.",
"defaultHoursMinutes": 900,
"defaultPricePaise": 1500000,
"defaultValidityDays": 90,
"isActive": true,
"displayOrder": 1
},
"sessions": [
{
"id": "bk_77aa11bb22",
"bookingNumber": "BK-RS-2606-S-44120-M",
"status": "completed",
"serviceType": "spa",
"bookingDate": "2026-06-10",
"startTime": "15:30:00",
"endTime": "17:30:00",
"totalAmountPaise": 0,
"totalDurationMinutes": 120,
"isMembershipSession": true,
"spaMembershipId": "mem_9aZ2bYc31",
"services": [
{
"bookingId": "bk_77aa11bb22",
"serviceId": "svc_aromamassage",
"serviceNameSnapshot": "Aromatherapy Massage",
"durationMinutes": 120,
"staffId": "stf_therapist01",
"staffName": "Anita R.",
"displayOrder": 0
}
]
}
]
}
}Errors
POST admin.theroyalglow.in/api/memberships/[id]/sessions
Records a SPA session against a membership. Each service line deducts its
duration from the remaining hours. The handler guards membership state and
remaining hours before any writes, validates that every service id exists,
snapshots service and staff names, then atomically creates a completed ₹0
booking flagged as a membership session (booking number suffixed -M), its
booking_service rows, a ₹0 membership_session invoice (zero gems), and
increments the membership's used minutes. bookingDate defaults to today (IST).
Minimum role: receptionist (requireRole('receptionist'))
POST /api/memberships/mem_9aZ2bYc31/sessions
Content-Type: application/jsonPath parameters
Prop
Type
Request body
Validated by recordSessionSchema (@rgss/types).
Prop
Type
{
"services": [
{
"serviceId": "svc_aromamassage",
"staffId": "stf_therapist01",
"durationMinutes": 120
}
],
"bookingDate": "2026-06-10"
}Response
Returns 201 Created with the created booking, the ₹0 session invoice, and the
membership's remaining minutes after the deduction.
{
"success": true,
"data": {
"booking": {
"id": "bk_77aa11bb22",
"bookingNumber": "BK-RS-2606-S-44120-M",
"branchId": "branch_rayasandra",
"customerId": "usr_abc123",
"status": "completed",
"serviceType": "spa",
"bookingDate": "2026-06-10",
"startTime": "15:30:00",
"endTime": "17:30:00",
"totalAmountPaise": 0,
"totalDurationMinutes": 120,
"isMembershipSession": true,
"spaMembershipId": "mem_9aZ2bYc31",
"completedAt": "2026-06-10T07:00:00.000Z"
},
"invoice": {
"id": "inv_55cc66dd77",
"invoiceNumber": "INV-1-2627-71044",
"branchId": "branch_rayasandra",
"bookingId": "bk_77aa11bb22",
"customerId": "usr_abc123",
"subtotalPaise": 0,
"taxableValuePaise": 0,
"gstAmountPaise": 0,
"totalAmountPaise": 0,
"invoiceType": "membership_session",
"paymentStatus": "paid",
"gemsEarned": 0,
"paidAt": "2026-06-10T07:00:00.000Z"
},
"remainingMinutes": 780
}
}Errors
POST admin.theroyalglow.in/api/memberships/[id]/cancel
Cancels a membership: sets status to cancelled and appends the reason to the
membership's notes. This is the only membership endpoint that requires
manager.
Minimum role: manager (requireRole('manager'))
POST /api/memberships/mem_9aZ2bYc31/cancel
Content-Type: application/jsonPath parameters
Prop
Type
Request body
Validated by cancelMembershipSchema (@rgss/types).
Prop
Type
{
"reason": "Customer relocated out of the city."
}Response
Returns the updated membership row.
{
"success": true,
"data": {
"id": "mem_9aZ2bYc31",
"membershipNumber": "RG-MEM-26-1-90872",
"customerId": "usr_abc123",
"tierId": "tier_gold",
"tierNameSnapshot": "Gold",
"totalHoursMinutes": 900,
"usedHoursMinutes": 120,
"pricePaidPaise": 1500000,
"startsAt": "2026-06-01T00:00:00.000Z",
"expiresAt": "2026-08-30T18:29:59.999Z",
"status": "cancelled",
"createdBy": "usr_reception01",
"invoiceId": "inv_3kP9qWx20",
"notes": "Cancelled: Customer relocated out of the city.",
"createdAt": "2026-06-01T05:30:00.000Z",
"updatedAt": "2026-06-12T08:00:00.000Z"
}
}Errors
Was this page helpful?