Appearance
Content, forms and contact
16. Content: CMS, blogs, events, forms, hubs
16.1 CMS content
A brand's app screens are published content, not app builds. The app asks for whatever is live for its platform and renders that, so a layout change ships without a release. Content is served by its own service and cached in Redis per brandId + platform + version.
GET /api/v2/brand/mobile/cms — the live content
Auth: Brand · Handler: getLiveContent (app/v2/controller/cms_controller.ts) Query: platform ✅ (string, min 1 — e.g. ios, android).
Returns 404, not the usual 200, when the brand has nothing published for that platform — so a client can tell "nothing published" apart from a failure. This is the endpoint the app renders from.
GET /api/v2/brand/mobile/cms/platform-state
Auth: Brand · Handler: getPlatformState · No params.
Which platforms have published content, and at what version. Poll it to decide whether the live content is worth re-fetching.
GET /api/v1/mobile/cms — the older content endpoint
Auth: Brand · Handler: getCMSByBrandId (cms_controller.js) Query: category, subCategory — comma-separated · name — split on ", " (comma + space) · version (string, default "0.0.0").
Still mounted, and still what brands predating the content service are served from. New mobile work reads the live content endpoint above instead.
16.2 GET /api/v1/mobile/blogs
Auth: Brand · Handler: getAllBlogs (blog_controller.js) Query: category (comma-separated keys of blogTypes, e.g. NEWS,NEW_RELEASES), tags (comma-separated), group_by (month | date), limit, offset, sortItem + sortType (comma-separated pairs of equal length; sortItem ∈ launchingDate, createdAt; sortType ∈ ASC, DESC).
16.3 GET /api/v1/mobile/blog/:blogId — path blogId (number) ✅ · Brand · getBlogById
16.4 GET /api/v1/mobile/blogs/:slug — path slug (string) ✅ · Brand · getBlogBySlug
⚠️
/mobile/blogs/:slugand/mobile/blogsare distinct routes, but a numeric-looking slug still resolves against:slug, not:blogId.
16.5 GET /api/v1/mobile/events (guest) · GET /api/v1/user/mobile/events (customer)
Handler: getAllEvents (event_controller.ts) — one handler, two mounts; the customer variant annotates per-user state.
Query (all optional): tags (comma-separated), limit, offset, search, group_by (month | date), includeExpired ("true"), expiredOnly ("true"), type (values of event_types: standard, drop), sortItem + sortType (comma-separated, equal length; sortItem ∈ start_date, end_date, createdAt, name). Default sort is relative to now: upcoming/in-progress first (soonest first), then finished (most recent first).
16.6 GET /api/v1/mobile/event/:event_id
Auth: Brand · Handler: getEventById · Path: event_id (number) ✅.
16.7 POST /api/v1/mobile/event/lead/:event_id (guest) · POST /api/v1/user/mobile/event/lead/:event_id (customer)
Handler: createEventLead — one handler, two mounts. Path: event_id (number) ✅. Body payload:
| field | type | req | notes |
|---|---|---|---|
email | string(email) | one-of | needs email or phone_number |
phone_number | string | one-of | requires phone_prefix (.with) |
phone_prefix | string | conditional | |
first_name, last_name, source, push_token | string | — | |
form_responses | array | — | { question_id ✅ (number), answer ✅ (any) }, default [] |
16.8 POST /api/v1/brand/mobile/form/response (customer) · POST /api/v1/brand/mobile/public/form/response (guest)
Handler: createFormResponse (form_controller.ts) — one handler, two mounts. The public variant records the response with no userId. Body payload: form_assignment_id ✅ (number), form_response ✅ (array of { question_id ✅ (number), answer ✅ (string) }).
16.9 GET /api/v1/brand/mobile/form/type
Auth: Brand · Handler: getFormByType Query — both required: id ✅ (number — the entity the form is attached to), type ✅ (keys of formType: scan_verify, scan_survey, product_review).
16.10 GET /api/v1/mobile/listed/hubs — store locator
Auth: Brand · Handler: getListedHubByBrand (listedHub_controller.js)
Query (all optional): show_on_nearest (boolean), limit, offset, search, lat + lng (must be sent together, .and), state, country (ISO code), city. limit falls back to the brand's siteConfiguration.listedHub.limit, then 10. Response: { count, limit, offset, ... } where count is the total match count.
⚠️
citytriggers the group filter but is not forwarded into it — onlystateandcountryactually filter. See §23.
The web twin of this route, and the admin endpoints that create the records it returns, are in Locations API.
17. Contact & newsletter
Source: routes/v1/contactus_route.js → contactus_controller.js
17.1 POST /api/v1/mobile/contact-us
Auth: Brand · Handler: addToContactUs · Body payload: email ✅ (email), message ✅ (string), firstName, lastName, emailType, phoneNumber (+ phone_prefix required with it), additional_data (any). Returns a message string, not a record.
17.2 POST /api/v1/user/mobile/newsletter
Auth: Customer · Handler: addToNewsLetter · Body payload:
| field | type | req | notes |
|---|---|---|---|
email | string(email) | ✅ | schema marks it required and has an or(email, phone_number) |
category | enum | ✅ | keys of newsletterTypes: NEWS, NEW_RELEASES, PRODUCT_UPDATE, COMPANY_ANNOUNCEMENT |
pointingId | string | conditional | required when category is PRODUCT_UPDATE or NEW_RELEASES |
first_name, last_name, source | string | — | |
phone_number | string | — | requires phone_prefix |