Skip to main content

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 by createLeadSchema (@rgss/types).

Response

Returns 201 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 by manualLeadSchema (@rgss/types) — extends createLeadSchema with source: 'manual' (literal, defaults to manual).

Response

Returns 201 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 by updateLeadStatusSchema (@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 by addLeadNoteSchema (@rgss/types).

Response

Returns 201 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.
Last modified on June 29, 2026