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

Leads

Public Meta-ad lead capture plus the admin lead pipeline (list, detail, status updates, notes).

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).

POST /api/leads
Content-Type: application/json

Request body

Validated by createLeadSchema (@rgss/types).

Prop

Type

{
  "name": "Priya Sharma",
  "phone": "9876543210",
  "email": "[email protected]",
  "serviceInterestedId": "svc_facialglow01",
  "utmSource": "facebook",
  "utmMedium": "paid_social",
  "utmCampaign": "monsoon_glow_2026",
  "utmContent": "carousel_v2",
  "utmTerm": "facial"
}

Response

Returns 201 Created with only the new lead id.

{
  "success": true,
  "data": {
    "leadId": "ld_7Qx2aMce91"
  }
}

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'))

GET /api/leads?status=new

Query parameters

Prop

Type

Response

{
  "success": true,
  "data": {
    "leads": [
      {
        "id": "ld_7Qx2aMce91",
        "name": "Priya Sharma",
        "phone": "+919876543210",
        "email": "[email protected]",
        "serviceInterestedId": "svc_facialglow01",
        "status": "new",
        "source": "meta_ad",
        "utmCampaign": "monsoon_glow_2026",
        "utmMedium": "paid_social",
        "utmSource": "facebook",
        "utmContent": "carousel_v2",
        "utmTerm": "facial",
        "assignedTo": null,
        "convertedBookingId": null,
        "lastContactedAt": null,
        "createdAt": "2026-06-01T05:30:00.000Z",
        "updatedAt": "2026-06-01T05:30:00.000Z",
        "serviceName": "Glow Facial",
        "daysSinceCapture": 0,
        "isStale": false
      }
    ]
  }
}

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'))

POST /api/leads
Content-Type: application/json

Request body

Validated by manualLeadSchema (@rgss/types) — extends createLeadSchema with source: 'manual' (literal, defaults to manual).

Prop

Type

{
  "name": "Walk-in Enquiry",
  "phone": "+919812345678",
  "serviceInterestedId": "svc_haircut001"
}

Response

Returns 201 Created with the new lead id.

{
  "success": true,
  "data": {
    "leadId": "ld_3Kp9bWfa20"
  }
}

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'))

GET /api/leads/ld_7Qx2aMce91

Path parameters

Prop

Type

Response

{
  "success": true,
  "data": {
    "lead": {
      "id": "ld_7Qx2aMce91",
      "name": "Priya Sharma",
      "phone": "+919876543210",
      "email": "[email protected]",
      "serviceInterestedId": "svc_facialglow01",
      "status": "contacted",
      "source": "meta_ad",
      "utmCampaign": "monsoon_glow_2026",
      "utmMedium": "paid_social",
      "utmSource": "facebook",
      "utmContent": "carousel_v2",
      "utmTerm": "facial",
      "assignedTo": "usr_reception01",
      "convertedBookingId": null,
      "lastContactedAt": "2026-06-01T09:10:00.000Z",
      "createdAt": "2026-06-01T05:30:00.000Z",
      "updatedAt": "2026-06-01T09:10:00.000Z",
      "serviceName": "Glow Facial",
      "assignedToName": "Reception Desk",
      "convertedBookingNumber": null
    },
    "notes": [
      {
        "id": "ldn_1aB2cD3eF4",
        "leadId": "ld_7Qx2aMce91",
        "content": "Called, interested in a weekend slot.",
        "authorId": "usr_reception01",
        "authorName": "Reception Desk",
        "createdAt": "2026-06-01T09:10:00.000Z"
      }
    ]
  }
}

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:

FromAllowed to
newcontacted, booked, lost
contactedfollow_up, booked, lost
follow_upbooked, lost
bookedwon, lost
won(terminal — no moves)
lost(terminal — no moves)

Same-status moves are not in the map and are therefore rejected.

PATCH /api/leads/ld_7Qx2aMce91
Content-Type: application/json

Path parameters

Prop

Type

Request body

Validated by updateLeadStatusSchema (@rgss/types).

Prop

Type

{
  "status": "lost",
  "reason": "Chose a competitor closer to home."
}

Response

Returns the updated lead row.

{
  "success": true,
  "data": {
    "lead": {
      "id": "ld_7Qx2aMce91",
      "name": "Priya Sharma",
      "phone": "+919876543210",
      "email": "[email protected]",
      "serviceInterestedId": "svc_facialglow01",
      "status": "lost",
      "source": "meta_ad",
      "utmCampaign": "monsoon_glow_2026",
      "utmMedium": "paid_social",
      "utmSource": "facebook",
      "utmContent": "carousel_v2",
      "utmTerm": "facial",
      "assignedTo": "usr_reception01",
      "convertedBookingId": null,
      "lastContactedAt": "2026-06-01T09:10:00.000Z",
      "createdAt": "2026-06-01T05:30:00.000Z",
      "updatedAt": "2026-06-02T11:00:00.000Z"
    }
  }
}

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'))

POST /api/leads/ld_7Qx2aMce91/notes
Content-Type: application/json

Path parameters

Prop

Type

Request body

Validated by addLeadNoteSchema (@rgss/types).

Prop

Type

{
  "content": "Left a voicemail, will retry tomorrow morning."
}

Response

Returns 201 Created with the new note.

{
  "success": true,
  "data": {
    "note": {
      "id": "ldn_5gH6iJ7kL8",
      "leadId": "ld_7Qx2aMce91",
      "authorId": "usr_reception01",
      "content": "Left a voicemail, will retry tomorrow morning.",
      "createdAt": "2026-06-02T04:00:00.000Z"
    }
  }
}

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.

OpenReport an issue

Was this page helpful?

On this page