Supercommerce API Docs
Admin API

Banner Module — Admin

HTTP surface for managing promotional banners attached to catalog entities (categories, brands, tags, ingredients). Banner targets a single entity via a polymorphic (entityType,…

HTTP surface for managing promotional banners attached to catalog entities (categories, brands, tags, ingredients). Banner targets a single entity via a polymorphic (entityType, entityId) pair and is consumed by an unauthenticated storefront endpoint owned by the same module.

Source: api-modules/banner/src/controllers/admin-banner.controller.ts.

Banner attaches to one of four catalog entity types. Catalog admin list endpoints surface a bannerCount so admins see how many banners are attached before deleting a brand/category.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching banner:* permission.

Endpoint groupPermission
GET /admin/banners, GET /admin/banners/:idbanner: read
POST /admin/bannersbanner: create
PUT /admin/banners/:id, POST /admin/banners/:id/restorebanner: update
DELETE /admin/banners/:idbanner: delete

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

Banners soft-delete via deletedAt. Hidden from default listings; POST /admin/banners/:id/restore reverses the deletion.


Domain types

BannerResponse

type BannerEntityType = "category" | "brand" | "tag" | "ingredient";
type Platform        = "APP" | "WEB" | "BOTH";

type BannerResponse = {
  id: string;
  entityType: BannerEntityType;
  entityId: string;
  image: string;                       // S3 storage key
  url: string;                         // click destination
  platform: Platform;
  isActive: boolean;
  sortOrder: number;                   // ascending; ties broken by createdAt desc
  metadata: Record<string, unknown> | null;
  createdAt: string;                   // ISO
  updatedAt: string;
  deletedAt: string | null;
};

URL rules

url is validated to be either an absolute https:// URL or a relative path that begins with /. Plain http://, javascript:, mailto: etc. are rejected.

Image storage

image is the S3 key returned by the storage module's presigned-upload flow. Banner module validates length only (1..1024 chars); the key format and existence are the storage module's contract.


Endpoints

Base path: /admin/banners.

GET /admin/banners — List banners

Required permission: banner: read. Standard QueryDto (page / limit / search / filters[]) plus first-class banner filters.

Query

NameTypeNotes
entityTypeBannerEntityType?One of category / brand / tag / ingredient
entityIdstring?Filter to one entity (typically combined with entityType)
platformPlatform?APP / WEB / BOTH
isActiveboolean?Coerced from true/false
page, limit, search, filters[]Standard QueryDto

Response 200 — paginated envelope of BannerResponse[].


GET /admin/banners/:id — Get a banner

Required permission: banner: read.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/banners — Create a banner

Required permission: banner: create.

Body

{
  "entityType": "brand",
  "entityId": "01J9...",
  "image": "banners/2026-05/festive.jpg",
  "url": "/sale",
  "platform": "BOTH",
  "isActive": true,
  "sortOrder": 0,
  "metadata": { "campaign": "Q2" }
}
FieldTypeConstraints
entityTypeenumcategory / brand / tag / ingredient
entityIdstringRequired; non-deleted row of the matching taxonomy table
imagestring1..1024 chars; storage key
urlstring1..2048 chars; https://... or relative /path
platformenumAPP / WEB / BOTH. Default BOTH
isActiveboolean?Default true
sortOrderint?>= 0. Default 0
metadataRecord<string, unknown> | null?Free-form

Response 201BannerResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (bad URL, unknown entity type, etc.)
404NOT_FOUNDentityId does not resolve to a non-deleted row of entityType

PUT /admin/banners/:id — Update a banner

Required permission: banner: update. entityType and entityId are immutable — to move a banner to a different entity, soft-delete and re-create.

Body — partial CreateBannerInput minus entityType / entityId.

Response 200BannerResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
400VALIDATION_ERRORBody fails zod

DELETE /admin/banners/:id — Soft-delete

Required permission: banner: delete. Stamps deletedAt. Hidden from storefront and default admin listings. Reversible via POST /:id/restore.

Response 200 — the deleted BannerResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/banners/:id/restore — Restore

Required permission: banner: update. Clears deletedAt.

Response 200BannerResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

Domain events

Emitted via EventEmitter2 after writes.

EventFired when
banner.createdPOST /admin/banners
banner.updatedPUT /admin/banners/:id, POST /admin/banners/:id/restore
banner.deletedDELETE /admin/banners/:id

  • admin-rbac — gates every endpoint via banner:* permissions. See admin-rbac.md.
  • catalog — owns the four entity tables (category, brand, tag, ingredient) that banners reference. Catalog admin lists surface a bannerCount aggregated from this table. See catalog.md.
  • storage — issues presigned-upload URLs for image keys and resolves them to CDN URLs on the storefront.

On this page