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

Loyalty & Offers

Customer gems balance and catalogue, public active offers, and admin offer management.

Loyalty & Offers

Two related areas: the gems loyalty programme (a customer's own balance, transaction history, and the redeemable-services catalogue) and offers (a public list of active promotions plus manager-only CRUD).

Base URLs: the customer endpoints (GET /api/gems, public GET /api/offers) are served from https://theroyalglow.in. The admin offer management endpoints are served from https://admin.theroyalglow.in with no /api/admin/ segment — the subdomain is the namespace (e.g. admin.theroyalglow.in/api/offers). Note /api/offers therefore exists in both apps: a public read on theroyalglow.in and role-gated CRUD on admin.theroyalglow.in. Money is an integer in paise (₹1,000.00 = 100000). Calendar dates are YYYY-MM-DD on input; date-mode columns serialise back as UTC-midnight ISO strings (e.g. "2026-06-30T00:00:00.000Z"). The role hierarchy is customer < staff < receptionist < manager < owner < developer.

Gems rules. Customers earn 1 gem per ₹100 invoiced (floor), on service invoices only — never on membership purchases or sessions. Gems expire 365 days after they are earned. Redemption is against specific catalogue services, not a rupee discount, and gems cannot be combined with an offer on the same booking.

GET theroyalglow.in/api/gems

Returns the signed-in customer's own loyalty summary, a paginated transaction history, and the redeemable-services catalogue. Strictly scoped to session.user.id — it never exposes another customer's data. A loyalty account is created on first call, so a brand-new customer sees zeros rather than an error.

Minimum role: customer (requireSession)

GET /api/gems?page=1&pageSize=20

Query parameters

Prop

Type

Response

data.summary is the balance plus lifetime totals (zeros if the account was just created). data.transactions are newest-first; invoiceNumber is null for non-invoice transactions (e.g. expired/adjusted). data.redeemable is the active gems-redeemable services catalogue. meta carries page and totalPages — there is no separate count query, so totalPages is page + 1 whenever a full page is returned (there may be more), otherwise page.

{
  "success": true,
  "data": {
    "summary": {
      "balance": 240,
      "totalEarned": 310,
      "totalRedeemed": 70
    },
    "transactions": [
      {
        "id": "lt_9c1f2a7b3d",
        "type": "earned",
        "gemsAmount": 13,
        "description": "Earned on invoice INV-1-2627-92921",
        "expiresAt": "2027-06-01T06:30:00.000Z",
        "createdAt": "2026-06-01T06:30:00.000Z",
        "invoiceNumber": "INV-1-2627-92921"
      },
      {
        "id": "lt_5a8d0e2c4f",
        "type": "redeemed",
        "gemsAmount": -70,
        "description": "Redeemed against Classic Manicure",
        "expiresAt": null,
        "createdAt": "2026-05-20T11:05:00.000Z",
        "invoiceNumber": null
      }
    ],
    "redeemable": [
      {
        "id": "svc_manicure01",
        "name": "Classic Manicure",
        "gemsRequired": 70,
        "pricePaise": 70000
      }
    ]
  },
  "meta": {
    "page": 1,
    "totalPages": 1
  }
}

loyalty_tx_type is one of earned, redeemed, expired, adjusted.

Errors


GET theroyalglow.in/api/offers

Public list of active offers for the customer offers page. No auth. Returns only offers whose isActive flag is true and whose calendar date range includes today, ordered by displayOrder. Each offer carries its linked services.

Minimum role: Public (no session required)

GET /api/offers

Response

Each offer is the full offer row plus services ({ id, name }[]) and the convenience arrays serviceIds and serviceNames. The discount field that is populated depends on offerType:

  • percentagediscountPercentage (1–100)
  • flatdiscountAmountPaise (paise)
  • combo_pricecomboPricePaise (paise)
{
  "success": true,
  "data": {
    "offers": [
      {
        "id": "of_3b7e1d9a2c",
        "name": "Monsoon Glow 20% Off",
        "slug": "monsoon-glow-20-off-a1b2c3",
        "description": "20% off select salon services this monsoon.",
        "offerType": "percentage",
        "discountPercentage": 20,
        "discountAmountPaise": null,
        "comboPricePaise": null,
        "startDate": "2026-06-01T00:00:00.000Z",
        "endDate": "2026-06-30T00:00:00.000Z",
        "isActive": true,
        "terms": "One offer per customer per day. Cannot combine with gems.",
        "imageUrl": null,
        "displayOrder": 0,
        "createdAt": "2026-05-28T09:00:00.000Z",
        "updatedAt": "2026-05-28T09:00:00.000Z",
        "services": [
          { "id": "svc_haircut001", "name": "Signature Haircut" }
        ],
        "serviceIds": ["svc_haircut001"],
        "serviceNames": ["Signature Haircut"]
      }
    ]
  }
}

Errors


GET admin.theroyalglow.in/api/offers

Lists all offers (admin view), newest first, each with its linked services.

Minimum role: manager (requireRole('manager'))

GET /api/offers

Response

Same per-offer shape as GET /api/offers, but unfiltered (includes inactive and out-of-range offers), ordered by createdAt descending.

{
  "success": true,
  "data": {
    "offers": [
      {
        "id": "of_3b7e1d9a2c",
        "name": "Monsoon Glow 20% Off",
        "slug": "monsoon-glow-20-off-a1b2c3",
        "description": "20% off select salon services this monsoon.",
        "offerType": "percentage",
        "discountPercentage": 20,
        "discountAmountPaise": null,
        "comboPricePaise": null,
        "startDate": "2026-06-01T00:00:00.000Z",
        "endDate": "2026-06-30T00:00:00.000Z",
        "isActive": true,
        "terms": "One offer per customer per day.",
        "imageUrl": null,
        "displayOrder": 0,
        "createdAt": "2026-05-28T09:00:00.000Z",
        "updatedAt": "2026-05-28T09:00:00.000Z",
        "services": [{ "id": "svc_haircut001", "name": "Signature Haircut" }],
        "serviceIds": ["svc_haircut001"],
        "serviceNames": ["Signature Haircut"]
      }
    ]
  }
}

Errors


POST admin.theroyalglow.in/api/offers

Creates an offer and its service links. The slug is derived server-side from the name (lowercased, hyphenated, with a short nanoid suffix so duplicate names never collide). The calendar dates are converted to UTC-midnight Dates.

Minimum role: manager (requireRole('manager'))

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

Request body

Validated by createOfferSchema (@rgss/types). The discount field that matches offerType is required, and endDate must be on or after startDate (both enforced via superRefine, surfaced as field-level errors).

Prop

Type

Requires discountPercentage.

{
  "name": "Monsoon Glow 20% Off",
  "offerType": "percentage",
  "discountPercentage": 20,
  "startDate": "2026-06-01",
  "endDate": "2026-06-30",
  "serviceIds": ["svc_haircut001"],
  "description": "20% off select salon services this monsoon.",
  "terms": "One offer per customer per day. Cannot combine with gems."
}

Requires discountAmountPaise.

{
  "name": "Flat ₹500 Off Styling",
  "offerType": "flat",
  "discountAmountPaise": 50000,
  "startDate": "2026-07-01",
  "endDate": "2026-07-15",
  "serviceIds": ["svc_haircut001", "svc_blowdry01"]
}

Requires comboPricePaise.

{
  "name": "Haircut + Beard Combo ₹999",
  "offerType": "combo_price",
  "comboPricePaise": 99900,
  "startDate": "2026-07-01",
  "endDate": "2026-07-31",
  "serviceIds": ["svc_haircut001", "svc_beardtrim01"]
}

Response

Returns 201 Created with the created offer (the raw offer row).

{
  "success": true,
  "data": {
    "offer": {
      "id": "of_3b7e1d9a2c",
      "name": "Monsoon Glow 20% Off",
      "slug": "monsoon-glow-20-off-a1b2c3",
      "description": "20% off select salon services this monsoon.",
      "offerType": "percentage",
      "discountPercentage": 20,
      "discountAmountPaise": null,
      "comboPricePaise": null,
      "startDate": "2026-06-01T00:00:00.000Z",
      "endDate": "2026-06-30T00:00:00.000Z",
      "isActive": true,
      "terms": "One offer per customer per day. Cannot combine with gems.",
      "imageUrl": null,
      "displayOrder": 0,
      "createdAt": "2026-05-28T09:00:00.000Z",
      "updatedAt": "2026-05-28T09:00:00.000Z"
    }
  }
}

Errors


GET admin.theroyalglow.in/api/offers/[id]

Returns a single offer with its linked services.

Minimum role: manager (requireRole('manager'))

GET /api/offers/of_3b7e1d9a2c

Path parameters

Prop

Type

Response

Same per-offer shape as the list endpoints (offer row plus services, serviceIds, serviceNames).

{
  "success": true,
  "data": {
    "offer": {
      "id": "of_3b7e1d9a2c",
      "name": "Monsoon Glow 20% Off",
      "slug": "monsoon-glow-20-off-a1b2c3",
      "offerType": "percentage",
      "discountPercentage": 20,
      "discountAmountPaise": null,
      "comboPricePaise": null,
      "startDate": "2026-06-01T00:00:00.000Z",
      "endDate": "2026-06-30T00:00:00.000Z",
      "isActive": true,
      "services": [{ "id": "svc_haircut001", "name": "Signature Haircut" }],
      "serviceIds": ["svc_haircut001"],
      "serviceNames": ["Signature Haircut"]
    }
  }
}

Errors


PATCH admin.theroyalglow.in/api/offers/[id]

Updates offer fields or toggles isActive. A bare { "isActive": false } (with no other fields) deactivates via the dedicated query; any other combination maps the provided fields (dates converted to Date) and updates. When serviceIds is supplied, the offer's service set is fully replaced.

Minimum role: manager (requireRole('manager'))

PATCH /api/offers/of_3b7e1d9a2c
Content-Type: application/json

Path parameters

Prop

Type

Request body

Validated by updateOfferSchema — every field from createOfferSchema is optional, plus an optional isActive boolean. The conditional discount/date refinements that apply on create are not re-run here (the base object is made partial).

Prop

Type

isActive: false alone deactivates the offer.

{
  "isActive": false
}
{
  "name": "Monsoon Glow 25% Off",
  "discountPercentage": 25,
  "endDate": "2026-07-15"
}

Response

Returns the updated offer row.

{
  "success": true,
  "data": {
    "offer": {
      "id": "of_3b7e1d9a2c",
      "name": "Monsoon Glow 25% Off",
      "slug": "monsoon-glow-20-off-a1b2c3",
      "offerType": "percentage",
      "discountPercentage": 25,
      "discountAmountPaise": null,
      "comboPricePaise": null,
      "startDate": "2026-06-01T00:00:00.000Z",
      "endDate": "2026-07-15T00:00:00.000Z",
      "isActive": true,
      "displayOrder": 0,
      "createdAt": "2026-05-28T09:00:00.000Z",
      "updatedAt": "2026-06-05T10:00:00.000Z"
    }
  }
}

Errors

OpenReport an issue

Was this page helpful?

On this page