Banners
In one line: the list of promo banners on the client app's home screen, and what each one does when tapped.
When you'd use this
Marketing creates banners in the operator tool — "20% off this weekend," "New: Burger Bros," "Try the closest branch." This endpoint gives the app the banners that are live right now, in the order they should be shown. Each banner also says what a tap should do: open a link, open a specific store, or find the customer's nearest branch of a brand.
Call it when the home screen (or a service's screen, like Groceries) loads.
Example: rendering the home screen
Ids and names below are illustrative.
-
The app opens the home screen and calls
GET /api/v2/client/banners. -
Three banners come back, highest
sequencefirst:Banner link_typeWhat the tap does "Weekend deals" urlOpens https://…/weekendfrom the banner'slink."Burger Bros JBR" storeOpens store 1874(from the banner'smerchant_idfield — see Watch out)."Order Burger Bros" nearest_branchCalls Nearest store with nearest_branch_merchant_id = 42and opens whichever branch it returns. -
The customer switches to the Groceries tab. The app calls the same endpoint again with that tab's
service_idto get the Groceries banners.
A banner that is still a draft, has expired, or hasn't started yet never shows up — the app doesn't need to filter anything.
How to call it
GET /api/v2/client/banners
Endpoint:
GET /api/v2/client/banners— OpenAPI operationClientBannersController_getBannersListinopenapi/order-v2-client.json, where the exact request and response fields live.
- For banners not tied to any service, call it without
service_id. For a service's own banners, pass that service'sservice_id. - Render in the order returned.
- On tap, switch on
link_type(table above).
Watch out
- No
service_id≠ all banners. It means "only banners that aren't tied to a service." - A banner's
merchant_idis actually a store id. Fornearest_branchbanners, usenearest_branch_merchant_idinstead. - An empty list might be a server error. Errors come back as
200with zero banners. If the home screen is unexpectedly empty, check the server logs.
Details (for backend)
Everything below is the reasoning and the exact rules — read it when you're changing this endpoint or debugging a surprising result.
Module: src/modules/client/banners/. Backed by src/modules/core/banners/banners.service.ts and
the AhsanBanner entity (src/modules/locateMerchantV2/entities/banner/banner.entity.ts), reading
the same banners table the operator API in locate-merchant-v2-api writes to.
What comes back, and why
- Only published banners in their active window are returned. A banner needs
status = 'publish', and "now" has to fall betweenstarts_atandexpires_at— anullstarts_atcounts as already started, anullexpires_atcounts as never expiring, and both bounds are inclusive. Draft banners and expired/not-yet-started ones never reach the client. (src/modules/core/banners/banners.service.ts,processQueryBuilder.) - Omitting
service_iddoes not mean "all banners." Passservice_idand you get banners scoped to that system category; leave it out and the query filters to banners whoseservice_idis null — the unscoped ones. There's no query that returns every banner regardless of scope in one call. (Same file —if (service_id) {...} else { queryBuilder.andWhere({ service_id: IsNull() }) }.) - Ordering is by
sequence, descending. Not bystarts_at, not by id. latitude/longitudeare accepted on the request but not used.QueryBannerDtodeclares them (src/modules/core/banners/dto/query-banner.dto.ts), butprocessQueryBuildernever reads them and they play no part in the Redis cache key either — sending them changes nothing about the response. Not documented as intentional; treat it as dead parameters unless someone confirms otherwise.- Caching is narrow. Only the exact request
?assigned=unassigned&language=<lang>is served from Redis (30s TTL, key built insrc/shared/helper/client-read-api-cache.helper.ts,getClientBannersCacheKey). Any other query — including everyservice_id-scoped request — always hits the database. - A database error looks like "no banners," not an error.
ClientBannersService.getBannerscatches everything and falls back to an empty, zero-item paginated payload with a200. If the home screen is unexpectedly empty, check the server logs before assuming there are genuinely no active banners — this also covers the case where a required column (e.g.link_type,target_merchant_id) hasn't been migrated into an environment yet; seedocs/sql/banners-link-type-target-merchant.sqlinlocate-order-v2-apifor that deploy-order hazard.
link_type and where a tap goes
Every banner has a link_type, defaulting to url for all pre-existing banners:
link_type | Behavior |
|---|---|
url | Open the banner's link field verbatim. |
store | Open the store in the banner's merchant_id field. |
nearest_branch | Call Nearest store with the banner's nearest_branch_merchant_id and the user's coordinates; open the store_id it returns, or do nothing if it's null. |
The merchant_id field on a banner is not a merchant id. It's a getter over the banner's
store_id column (get merchant_id() { return this.store_id || null; } in banner.entity.ts), kept
under its old name for backward compatibility. The one field that is a real merchants.id is
nearest_branch_merchant_id, and it's only ever set when link_type is nearest_branch. Passing
the wrong one into Nearest store's :merchantId doesn't error — it comes back
200 with a null store_id, which looks exactly like "no branch nearby." Full hazard table on the
Nearest store page.
Not documented yet
- Whether
latitude/longitudewere meant to drive some future "banners near me" filter, or are simply unused leftovers. - Whether a
store-type banner's target store is checked for being active/open before the client opens it (unlikenearest_branch, which explicitly filters to active stores) — not established from this endpoint's code; the filtering, if any, would happen client-side or in the store-detail call itself.