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
| Term | Meaning |
|---|---|
publicId | A uuid7 string identifying a resource. Used in all URL paths and response bodies instead of integer IDs. |
slug | URL-safe identifier for a placement (e.g., homepage-hero). Used in the public serving endpoint. |
scheduleStatus | Banner activation state: evergreen / scheduled / active / expired. |
campaignStatus | Campaign lifecycle state: draft / scheduled / active / paused / ended / cancelled. |
targeting context | HTTP header values extracted from the client request to evaluate targeting rules. |
correlationId | Client-generated UUID to deduplicate impression/click events. Used as BullMQ jobId for tracking. |
paisa | NPR 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/bannersAuth: JWT + Banners_CREATE
Request body fields (see apps/api/src/modules/banners/admin/banners/dto/create-banner.dto.ts for full DTO):
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | Yes | Max 255 chars |
description | string | No | |
source | "internal" | "sponsored" | "affiliate" | "system" | No | Default internal |
primaryImageUrl | string | Yes | |
primaryImageAlt | string | No | Max 255 |
mobileImageUrl | string | No | |
videoUrl | string | No | |
mediaVariants | Record<string, string> | No | Per-locale/per-size variant URLs |
headline / subheadline | string | No | Max 255 |
ctaLabel | string | No | Max 100 |
ctaUrl | string (URL) | No | |
ctaOpenNewTab | boolean | No | Default false |
utmSource / utmMedium / utmCampaign / utmContent | string | No | Max 100 |
brandId / categoryId / productId | number | No | FK integers |
tags | string[] | No | |
bgColor | string | No | Hex #RRGGBB |
priority | number | No | Default 0 |
seoId | string (UUID) | No | FK to SEO module |
publishAt / expiresAt | string (ISO 8601) | No | |
scheduleTimezone | string | No | IANA tz, default UTC |
recurrenceRule | string | No | |
recurrenceWindowStart / recurrenceWindowEnd | string (HH:MM:SS) | No | |
scheduleStatus | "evergreen" | "scheduled" | "active" | "expired" | No | Default evergreen |
isEvergreenFallback | boolean | No | |
fallbackPlaceholderUrl | string | No |
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/bannersAuth: 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/:publicIdAuth: JWT + Banners_READ
Update Banner
PATCH /api/admin/banners/:publicIdAuth: JWT + Banners_UPDATE
Partial update; invalidates list cache on success.
Soft-Delete Banner
DELETE /api/admin/banners/:publicIdAuth: JWT + Banners_DELETE
Errors: BANNER_ALREADY_DELETED (400 if already deleted).
Restore Banner
POST /api/admin/banners/:publicId/restoreAuth: JWT + Banners_UPDATE
Errors: BANNER_NOT_DELETED (400 if not deleted).
Update Schedule
PATCH /api/admin/banners/:publicId/scheduleAuth: JWT + Banners_UPDATE
Body: ScheduleBannerDto (scheduleStatus, publishAt, expiresAt, recurrence fields).
Activate / Deactivate
PATCH /api/admin/banners/:publicId/activate
PATCH /api/admin/banners/:publicId/deactivateAuth: JWT + Banners_UPDATE
Admin — Placement Endpoints
Base path: /api/admin/placements
| Action | Endpoint | Permission |
|---|---|---|
| List placements | GET /admin/placements | Placements_READ |
| Get placement | GET /admin/placements/:publicId | Placements_READ |
| Create placement | POST /admin/placements | Placements_CREATE |
| Update placement | PATCH /admin/placements/:publicId | Placements_UPDATE |
| Soft-delete placement | DELETE /admin/placements/:publicId | Placements_DELETE |
| Activate | PATCH /admin/placements/:publicId/activate | Placements_UPDATE |
| Deactivate | PATCH /admin/placements/:publicId/deactivate | Placements_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
| Action | Endpoint | Permission |
|---|---|---|
| List campaigns | GET /admin/campaigns | Campaigns_READ |
| Get campaign | GET /admin/campaigns/:publicId | Campaigns_READ |
| Create campaign | POST /admin/campaigns | Campaigns_CREATE |
| Update campaign | PATCH /admin/campaigns/:publicId | Campaigns_UPDATE |
| Soft-delete campaign | DELETE /admin/campaigns/:publicId | Campaigns_DELETE |
| Restore campaign | POST /admin/campaigns/:publicId/restore | Campaigns_UPDATE |
| Schedule | PATCH /admin/campaigns/:publicId/schedule | Campaigns_UPDATE |
| Activate | PATCH /admin/campaigns/:publicId/activate | Campaigns_UPDATE |
| Pause | PATCH /admin/campaigns/:publicId/pause | Campaigns_UPDATE |
| End | PATCH /admin/campaigns/:publicId/end | Campaigns_UPDATE |
| Cancel | PATCH /admin/campaigns/:publicId/cancel | Campaigns_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).
Admin — Campaign Placement Links
Nested under campaigns: /api/admin/campaigns/:publicId/placements
| Action | Endpoint | Permission |
|---|---|---|
| Add placement to campaign | POST /admin/campaigns/:publicId/placements | Assignments_CREATE |
| Remove placement from campaign | DELETE /admin/campaigns/:publicId/placements/:placementPublicId | Assignments_DELETE |
| Activate placement link | PATCH /admin/campaigns/:publicId/placements/:placementPublicId/activate | Assignments_UPDATE |
| Deactivate placement link | PATCH /admin/campaigns/:publicId/placements/:placementPublicId/deactivate | Assignments_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
| Action | Endpoint | Permission |
|---|---|---|
| List banner assignments | GET /admin/campaigns/:publicId/placements/:placementPublicId/banners | Assignments_READ |
| Assign banner to slot | POST /admin/campaigns/:publicId/placements/:placementPublicId/banners | Assignments_CREATE |
| Update banner assignment | PATCH /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicId | Assignments_UPDATE |
| Remove banner assignment | DELETE /admin/campaigns/:publicId/placements/:placementPublicId/banners/:bannerPublicId | Assignments_DELETE |
| Activate | PATCH .../banners/:bannerPublicId/activate | Assignments_UPDATE |
| Deactivate | PATCH .../banners/:bannerPublicId/deactivate | Assignments_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
| Action | Endpoint | Permission |
|---|---|---|
| List targeting rules | GET /admin/targeting-rules?campaignPublicId=... | TargetingRules_READ |
| Add targeting rule | POST /admin/targeting-rules | TargetingRules_CREATE |
| Update targeting rule | PATCH /admin/targeting-rules/:publicId | TargetingRules_UPDATE |
| Delete targeting rule | DELETE /admin/targeting-rules/:publicId | TargetingRules_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/:slugAuth: None. @Public() class decorator bypasses global JwtAuthGuard.
Rate limit: 300 requests/minute per IP (configurable via @IpThrottle).
URL params:
| Param | Type | Notes |
|---|---|---|
slug | string | Placement slug (e.g., homepage-hero) |
Headers used for targeting (all optional):
| Header | Used For |
|---|---|
User-Agent | Device 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-Language | Primary language tag (lowercased, split on [-;]) |
x-device-id (or x-session-id) | Absence → isNewVisitor = true |
Referer | Extracted 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/impressionAuth: 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/clickSame 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.