Shop It Docs
Developer Resourcesbanners

Banners API Reference

Banners Module — API Reference

Audience: Frontend engineers, mobile engineers, API consumers integrating with the Banners module.

All endpoints are prefixed with /api. Admin endpoints require a valid JWT and the appropriate permission. Public endpoints are accessible without authentication (@Public() class decorator bypasses the global JwtAuthGuard).


Concepts and Terminology

TermMeaning
publicIdA uuid7 string identifying a resource. Used in all URL paths and response bodies instead of integer IDs.
slugURL-safe identifier for a placement (e.g., homepage-hero). Used in the public serving endpoint.
scheduleStatusBanner activation state: evergreen / scheduled / active / expired.
campaignStatusCampaign lifecycle state: draft / scheduled / active / paused / ended / cancelled.
targeting contextHTTP header values extracted from the client request to evaluate targeting rules.
correlationIdClient-generated UUID to deduplicate impression/click events. Used as BullMQ jobId for tracking.
paisaNPR minor unit (100 paisa = 1 NPR). All monetary values in API use paisa (integer).

Admin — Banner Endpoints

Base path: /api/admin/banners

Create Banner

POST /api/admin/banners

Auth: JWT + Banners_CREATE

Request body fields (see apps/api/src/modules/banners/admin/banners/dto/create-banner.dto.ts for full DTO):

FieldTypeRequiredNotes
titlestringYesMax 255 chars
descriptionstringNo
source"internal" | "sponsored" | "affiliate" | "system"NoDefault internal
primaryImageUrlstringYes
primaryImageAltstringNoMax 255
mobileImageUrlstringNo
videoUrlstringNo
mediaVariantsRecord<string, string>NoPer-locale/per-size variant URLs
headline / subheadlinestringNoMax 255
ctaLabelstringNoMax 100
ctaUrlstring (URL)No
ctaOpenNewTabbooleanNoDefault false
utmSource / utmMedium / utmCampaign / utmContentstringNoMax 100
brandId / categoryId / productIdnumberNoFK integers
tagsstring[]No
bgColorstringNoHex #RRGGBB
prioritynumberNoDefault 0
seoIdstring (UUID)NoFK to SEO module
publishAt / expiresAtstring (ISO 8601)No
scheduleTimezonestringNoIANA tz, default UTC
recurrenceRulestringNo
recurrenceWindowStart / recurrenceWindowEndstring (HH:MM:SS)No
scheduleStatus"evergreen" | "scheduled" | "active" | "expired"NoDefault evergreen
isEvergreenFallbackbooleanNo
fallbackPlaceholderUrlstringNo

Response: 200 OK with ResponseDto<BannerAdminResponseDto>. Service sets createdBy from the JWT admin id.

Errors: BANNER_SEO_NOT_FOUND (404), BANNER_SCHEDULE_INVALID (400 — publishAt >= expiresAt).

List Banners

GET /api/admin/banners

Auth: JWT + Banners_READ

Query params (see fetch-banners-admin.dto.ts): page, size, sort (updatedAt/createdAt/priority/publishAt/expiresAt/title), order (asc/desc), search (title + description ILIKE), isActive (boolean), scheduleStatus, brandId, categoryId, includeDeleted (boolean).

Response: 200 OK paginated ResponseDto<BannerAdminResponseDto[]> with pagination && { count, page, size }.

Get Banner

GET /api/admin/banners/:publicId

Auth: JWT + Banners_READ

Update Banner

PATCH /api/admin/banners/:publicId

Auth: JWT + Banners_UPDATE

Partial update; invalidates list cache on success.

Soft-Delete Banner

DELETE /api/admin/banners/:publicId

Auth: JWT + Banners_DELETE

Errors: BANNER_ALREADY_DELETED (400 if already deleted).

Restore Banner

POST /api/admin/banners/:publicId/restore

Auth: JWT + Banners_UPDATE

Errors: BANNER_NOT_DELETED (400 if not deleted).

Update Schedule

PATCH /api/admin/banners/:publicId/schedule

Auth: JWT + Banners_UPDATE

Body: ScheduleBannerDto (scheduleStatus, publishAt, expiresAt, recurrence fields).

Activate / Deactivate

PATCH /api/admin/banners/:publicId/activate
PATCH /api/admin/banners/:publicId/deactivate

Auth: JWT + Banners_UPDATE


Admin — Placement Endpoints

Base path: /api/admin/placements

ActionEndpointPermission
List placementsGET /admin/placementsPlacements_READ
Get placementGET /admin/placements/:publicIdPlacements_READ
Create placementPOST /admin/placementsPlacements_CREATE
Update placementPATCH /admin/placements/:publicIdPlacements_UPDATE
Soft-delete placementDELETE /admin/placements/:publicIdPlacements_DELETE
ActivatePATCH /admin/placements/:publicId/activatePlacements_UPDATE
DeactivatePATCH /admin/placements/:publicId/deactivatePlacements_UPDATE

Request body fields (CreatePlacementDto): slug (unique), label, layoutType (10 enum values), pageContext, maxBanners (default 1), recommendedWidth/Height/AspectRatio, notes, isActive, sortOrder, fallbackPlaceholderUrl, allowPartialRender.

Errors: PLACEMENT_SLUG_EXISTS (409), PLACEMENT_HAS_ACTIVE_ASSIGNMENTS (409 on delete), PLACEMENT_NOT_FOUND (404).


Admin — Campaign Endpoints

Base path: /api/admin/campaigns

ActionEndpointPermission
List campaignsGET /admin/campaignsCampaigns_READ
Get campaignGET /admin/campaigns/:publicIdCampaigns_READ
Create campaignPOST /admin/campaignsCampaigns_CREATE
Update campaignPATCH /admin/campaigns/:publicIdCampaigns_UPDATE
Soft-delete campaignDELETE /admin/campaigns/:publicIdCampaigns_DELETE
Restore campaignPOST /admin/campaigns/:publicId/restoreCampaigns_UPDATE
SchedulePATCH /admin/campaigns/:publicId/scheduleCampaigns_UPDATE
ActivatePATCH /admin/campaigns/:publicId/activateCampaigns_UPDATE
PausePATCH /admin/campaigns/:publicId/pauseCampaigns_UPDATE
EndPATCH /admin/campaigns/:publicId/endCampaigns_UPDATE
CancelPATCH /admin/campaigns/:publicId/cancelCampaigns_UPDATE

Request body fields (CreateCampaignDto): name, description, source, startsAt, endsAt, budgetTotalPaisa, budgetDailyPaisa, impressionCap, clickCap, advertiserName, advertiserContact, contractRef, priority. Service sets status = "draft" regardless of body.

Errors: CAMPAIGN_NOT_FOUND (404), CAMPAIGN_DATE_INVALID (400), CAMPAIGN_INVALID_TRANSITION (400), CAMPAIGN_ALREADY_DELETED (400), CAMPAIGN_NOT_DELETED (400).


Nested under campaigns: /api/admin/campaigns/:publicId/placements

ActionEndpointPermission
Add placement to campaignPOST /admin/campaigns/:publicId/placementsAssignments_CREATE
Remove placement from campaignDELETE /admin/campaigns/:publicId/placements/:placementPublicIdAssignments_DELETE
Activate placement linkPATCH /admin/campaigns/:publicId/placements/:placementPublicId/activateAssignments_UPDATE
Deactivate placement linkPATCH /admin/campaigns/:publicId/placements/:placementPublicId/deactivateAssignments_UPDATE

Body: { placementPublicId, isActive? }. The service resolves publicId → integer id and uses composite (campaign_id, placement_id) PK.

Errors: CAMPAIGN_PLACEMENT_NOT_FOUND (404), CAMPAIGN_PLACEMENT_ALREADY_EXISTS (409).


Admin — Banner Assignments

Nested under campaign placements: /api/admin/campaigns/:publicId/placements/:placementPublicId/banners

ActionEndpointPermission
List banner assignmentsGET /admin/campaigns/:publicId/placements/:placementPublicId/bannersAssignments_READ
Assign banner to slotPOST /admin/campaigns/:publicId/placements/:placementPublicId/bannersAssignments_CREATE
Update banner assignmentPATCH /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicIdAssignments_UPDATE
Remove banner assignmentDELETE /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicIdAssignments_DELETE
ActivatePATCH .../banners/:bannerPublicId/activateAssignments_UPDATE
DeactivatePATCH .../banners/:bannerPublicId/deactivateAssignments_UPDATE

Body (AssignBannerDto): bannerPublicId, displayOrder? (default 0), weight? (default 100), isFallback?, fallbackPriority?, overrideCtaLabel/Url/Headline/Subheadline?, transitionDurationMs?.

UpdateBannerAssignmentDto = PartialType(OmitType(AssignBannerDto, ['bannerPublicId'] as const))bannerPublicId is in URL.

Errors: BANNER_ASSIGNMENT_NOT_FOUND (404), BANNER_ASSIGNMENT_ALREADY_EXISTS (409), CAMPAIGN_PLACEMENT_NOT_FOUND (404), BANNER_NOT_FOUND (404), PLACEMENT_NOT_FOUND (404), CAMPAIGN_NOT_FOUND (404).


Admin — Targeting Rule Endpoints

Base path: /api/admin/targeting-rules

ActionEndpointPermission
List targeting rulesGET /admin/targeting-rules?campaignPublicId=...TargetingRules_READ
Add targeting rulePOST /admin/targeting-rulesTargetingRules_CREATE
Update targeting rulePATCH /admin/targeting-rules/:publicIdTargetingRules_UPDATE
Delete targeting ruleDELETE /admin/targeting-rules/:publicIdTargetingRules_DELETE

Body fields: campaignPublicId, ruleType (9 enum values), operator (validated by TargetingAdminService.VALID_OPERATORS per rule_type), value (jsonb), isActive?.

Errors: TARGETING_RULE_NOT_FOUND (404), TARGETING_RULE_INVALID_OPERATOR (400), CAMPAIGN_NOT_FOUND (404).


Public — Serving Endpoint

Get Banners for Placement

GET /api/banners/serve/:slug

Auth: None. @Public() class decorator bypasses global JwtAuthGuard.

Rate limit: 300 requests/minute per IP (configurable via @IpThrottle).

URL params:

ParamTypeNotes
slugstringPlacement slug (e.g., homepage-hero)

Headers used for targeting (all optional):

HeaderUsed For
User-AgentDevice type detection (regex-based: iPad/Android(no Mobile) → tablet, Mobile/Android/iPhone/iPod → mobile, else desktop)
x-country-code (fallback: cf-ipcountry)ISO 3166-1 alpha-2 country code
Authorization (Bearer ... header present)isLoggedIn = true (token NOT verified — endpoint is @Public())
Accept-LanguagePrimary language tag (lowercased, split on [-;])
x-device-id (or x-session-id)Absence → isNewVisitor = true
RefererExtracted domain for referrer_domain rule

Response shape: ResponseDto<BannerServeResponseDto>:

{
  message: "Banners served successfully",
  data: {
    placement: {
      slug: string,
      label: string,
      layoutType: PlacementLayout,
      maxBanners: number,
      allowPartialRender: boolean,
      fallbackPlaceholderUrl: string | null,
    },
    banners: Array<{
      publicId: string,
      title: string,
      primaryImageUrl: string,
      primaryImageAlt: string | null,
      mobileImageUrl: string | null,
      videoUrl: string | null,
      mediaVariants: Record<string, string> | null,
      headline: string | null,
      subheadline: string | null,
      ctaLabel: string | null,
      ctaUrl: string | null,
      ctaOpenNewTab: boolean,
      utmSource: string | null,
      utmMedium: string | null,
      utmCampaign: string | null,
      utmContent: string | null,
      bgColor: string | null,
      transitionDurationMs: number | null,
      isFallback: boolean,
      displayOrder: number,
    }>,
    servedAt: string  // ISO 8601; always the live timestamp, NEVER cached
  }
}

Errors: PLACEMENT_NOT_FOUND (404). banners may be [] if no active campaigns match.


Public — Event Tracking Endpoints

Record Impression

POST /api/banners/events/impression

Auth: None. @Public(). Rate limit: 600 requests/minute per IP.

Body fields (see record-impression.dto.ts): bannerPublicId (UUID), placementPublicId (UUID), campaignPublicId? (UUID), sessionId? (max 128), userId? (UUID), deviceType? (max 20), countryCode? (max 2), pageUrl?, referrerUrl?, occurredAt (ISO 8601), correlationId (max 128).

Response: 202 Accepted with ResponseDto after enqueueing the job.

Service flow: resolves publicIds → integer IDs in parallel via Promise.all, then enqueues BannerJob.RECORD_IMPRESSION with jobId = correlationId. Duplicate correlationId silently no-ops (idempotent).

Errors: BANNER_NOT_FOUND (404), PLACEMENT_NOT_FOUND (404). Missing campaignPublicId is fine — campaignId is set to undefined without throwing.

Record Click

POST /api/banners/events/click

Same shape as impression, plus required destinationUrl (string). Inserts into banner_clicks table.

Response: 202 Accepted. Same idempotency semantics.


BullMQ Job Contracts

Defined in packages/jobs/src/index.ts:

export enum QueueName {
  // ...
  BANNERS = "banners",
}

export enum BannerJob {
  RECORD_IMPRESSION = "banner.record_impression",
  RECORD_CLICK = "banner.record_click",
  SYNC_SCHEDULE_STATUS = "banner.sync_schedule_status",
  ROLLUP_STATS = "banner.rollup_stats",
}

export interface RecordBannerImpressionPayload {
  bannerId: number;
  placementId: number;
  campaignId?: number;
  sessionId?: string;
  userId?: string;
  deviceType?: string;
  countryCode?: string;
  pageUrl?: string;
  referrerUrl?: string;
  occurredAt: string;     // ISO 8601
  correlationId: string;
}

export interface RecordBannerClickPayload {
  bannerId: number;
  placementId: number;
  campaignId?: number;
  sessionId?: string;
  userId?: string;
  deviceType?: string;
  countryCode?: string;
  destinationUrl?: string;
  pageUrl?: string;
  occurredAt: string;
  correlationId: string;
}

export interface SyncBannerScheduleStatusPayload {
  correlationId: string;
}

export interface RollupBannerStatsPayload {
  statDate: string;        // YYYY-MM-DD (UTC)
  correlationId: string;
}

export type BannerQueueJobs =
  | { name: BannerJob.RECORD_IMPRESSION; data: RecordBannerImpressionPayload }
  | { name: BannerJob.RECORD_CLICK; data: RecordBannerClickPayload }
  | { name: BannerJob.SYNC_SCHEDULE_STATUS; data: SyncBannerScheduleStatusPayload }
  | { name: BannerJob.ROLLUP_STATS; data: RollupBannerStatsPayload };

Common Response Envelope

All endpoints follow the shared ResponseDto<T> envelope:

{ message: string, data: T | null }

For paginated list responses, an additional pagination object is added: { count, page, size } (only when pagination: true in query).

Error responses (thrown via NotFoundException / BadRequestException / ConflictException):

{ statusCode: number, message: string, errorCode: "BANNER_NOT_FOUND" }

The frontend must rely on errorCode (not message text) for UI logic.