Leads
The lead surface has two halves.POST /api/leads on the customer site
(theroyalglow.in) is the only unauthenticated write endpoint in this
phase — it backs the /book Meta-ad landing page. The admin lead pipeline is a
separate app served from admin.theroyalglow.in (its endpoints are
admin.theroyalglow.in/api/leads*, with no /admin path prefix — the
subdomain is the namespace) and requires a Better Auth session with at least the
receptionist role.
Base URLs: customer capture is
https://theroyalglow.in/api/leads; the
admin pipeline endpoints are served from https://admin.theroyalglow.in
(e.g. admin.theroyalglow.in/api/leads, .../api/leads/[id]). There is no
/api/admin/ segment anywhere. Roles are hierarchical: customer <
staff < receptionist < manager < owner < developer. A handler that
calls requireRole('receptionist') also admits manager, owner, and
developer. Phone numbers are normalised to canonical +91XXXXXXXXXX in the
business layer before storage. Lead pipeline:
new → contacted → follow_up → booked → won/lost.POST theroyalglow.in/api/leads
Public lead capture from the Meta-ad landing page. The endpoint is per-IP
rate-limited and strictly Zod-validated. It deliberately echoes back nothing
beyond the created leadId (no PII). The source defaults to meta_ad when
omitted.
Minimum role: Public — no session required (rate-limited per IP: 5 requests
per 60s window).
Request body
Validated bycreateLeadSchema (@rgss/types).
Response
Returns201 Created with only the new lead id.
Errors
GET admin.theroyalglow.in/api/leads
Returns the lead pipeline, newest first. Each row is a flat lead enriched with
the service-interest name plus two computed fields: daysSinceCapture (whole
days since createdAt) and isStale (whether the lead has gone too long
without movement for its status). Pass ?status= to bucket a single column of
the kanban board.
Minimum role: receptionist (requireRole('receptionist'))
Query parameters
Response
Errors
POST admin.theroyalglow.in/api/leads
Creates a lead manually from inside the admin portal (e.g. a phone enquiry).
Identical body to the public endpoint, but source is fixed to manual by
manualLeadSchema and the handler — it always overrides source to manual.
Minimum role: receptionist (requireRole('receptionist'))
Request body
Validated bymanualLeadSchema (@rgss/types) — extends createLeadSchema
with source: 'manual' (literal, defaults to manual).
Response
Returns201 Created with the new lead id.
Errors
GET admin.theroyalglow.in/api/leads/[id]
Returns a single lead enriched with its service-interest name, assigned-to user
name, and the converted booking number (each null when absent), together with
its notes (newest first).
Minimum role: receptionist (requireRole('receptionist'))
Path parameters
Response
Errors
PATCH admin.theroyalglow.in/api/leads/[id]
Updates a lead’s status, enforcing the pipeline state machine. When the new
status is contacted, the handler also stamps lastContactedAt. Marking a lead
lost requires a non-empty reason.
Minimum role: receptionist (requireRole('receptionist'))
The allowed transitions (assertLeadTransition) are:
Same-status moves are not in the map and are therefore rejected.
Path parameters
Request body
Validated byupdateLeadStatusSchema (@rgss/types).
Response
Returns the updated lead row.Errors
POST admin.theroyalglow.in/api/leads/[id]/notes
Appends a note to a lead. The note’s author is taken from the session, not the
request body.
Minimum role: receptionist (requireRole('receptionist'))
Path parameters
Request body
Validated byaddLeadNoteSchema (@rgss/types).
Response
Returns201 Created with the new note.
Errors
Unlike
GET/PATCH on /api/leads/[id], this handler does not
pre-check that the lead exists. Posting a note for a non-existent id fails on
the foreign-key constraint and surfaces as INTERNAL_ERROR (500) rather than
NOT_FOUND.