Commercial rules
Promotional Banners
Promotional banners are image-based calls to action displayed on KIOSK or Web Ordering (CEL). You can manage local restaurant banners, distribute brand banners to several restaurants, expose the active display feeds to guest applications, and report engagement.
status, code,message, and data. The message is localized; integrations should use the HTTP status and code. Request schemas using Joi strip unknown JSON properties before the controller receives them.Access and terminology
| Endpoint group | Authentication and allowed roles |
|---|---|
| Restaurant banner CRUD, duplicate, and per-restaurant toggle | Bearer authentication required; Admin, Brand, or Restaurant. |
| Brand banner CRUD, carousel settings, targets, CEL products, and pickup schedules | Bearer authentication required; Admin or Brand. |
| Active restaurant feed, active food-court feed, and event tracking | Guest-accessible; no bearer token is required. The ACL also permits Admin, Brand, Restaurant, Kiosk, and Customer roles. |
channelIdis1for KIOSK and2for WEB/CEL.placementisrestaurantsfor per-restaurant display orfoodcourt_homefor the brand-wide food-court landing page.- Local banners store a category or product ID in
linkTargetId. Brand banners store a reusable reference inlinkTargetRef: a product SKU or a category name.
Restaurant banner management
These endpoints manage banners owned directly by a restaurant. The list also includes brand banners targeted to that restaurant; their returned isActive value is the target restaurant’s effective activation state.
GET/promotional_banners/restaurant/{restaurantId}List restaurant banners
Requires Bearer authentication and an Admin, Brand, or Restaurant role. Returns local banners plus brand banners targeted to the restaurant. For a brand banner, isActive is the activation state of that restaurant target.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| restaurantId | integer | Yes | Restaurant identifier (path parameter). |
| sort | string | No | Optional query parameter. Use `name`, `status`, `linkType`, or `created_at`, as `field:asc`, `field:desc`, `-field`, or a comma-separated combination. Unknown fields are ignored; default order is created_at ascending. |
Response
{
"status": 200,
"code": "list_promotional_banners_succeed",
"message": "Promotional banners fetched successfully",
"data": [
{
"promotionalBannerId": 901,
"name": "Lunch special",
"imageId": 501,
"isActive": true,
"linkType": "product",
"linkTargetId": 5001,
"linkTargetRef": null,
"channelId": 1,
"placement": "restaurants",
"restaurantId": 42,
"brandId": null,
"created_at": "2026-07-11T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 501,
"fullUrl": "https://cdn.example.com/promotional-banners/lunch-special.jpg"
}
},
{
"promotionalBannerId": 902,
"name": "Brand summer offer",
"imageId": 502,
"isActive": true,
"linkType": "product",
"linkTargetId": null,
"linkTargetRef": "SUMMER-BURGER",
"channelId": 1,
"placement": "restaurants",
"restaurantId": null,
"brandId": 7,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 502,
"fullUrl": "https://cdn.example.com/promotional-banners/brand-summer.jpg"
},
"promotional_banner_brands": [
{
"promotionalBannerId": 902,
"restaurantId": 42,
"isActive": true,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z"
},
{
"promotionalBannerId": 902,
"restaurantId": 43,
"isActive": false,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z"
}
]
}
]
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_promotional_banners_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banners fetched successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | array | Yes | […] | Local and targeted brand banner records. |
| data[] | object | Not specified | {…} | Endpoint-specific response payload. |
| data[].promotionalBannerId | integer | Yes | 901 | Unique identifier of the promotional banner. |
| data[].name | string | Yes | "Lunch special" | Human-readable banner name. |
| data[].imageId | integer | Yes | 501 | Identifier of the image selected for the banner. |
| data[].isActive | boolean | Yes | true | For a local banner, its own activation state. For a brand banner, the activation state of this requested restaurant target. |
| data[].linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data[].linkTargetId | integer | null | Yes | 5001 | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data[].linkTargetRef | string | null | Yes | null | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data[].channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data[].placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data[].restaurantId | integer | null | Yes | 42 | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data[].brandId | integer | null | Yes | null | Owning brand for a brand-scoped banner; null for a local banner. |
| data[].created_at | string | Yes | "2026-07-11T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data[].updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data[].deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data[].image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data[].image.imageId | integer | No | 501 | Identifier of the resolved image. |
| data[].image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/lunch-special.jpg" | Absolute URL of the resolved banner image. |
| data[].promotional_banner_brands | array | No | Not provided | Per-restaurant target rows. This property is present for brand banners in list responses, not local banners. |
| data[].promotional_banner_brands[] | object | No | Not provided | One brand-banner assignment to a restaurant. |
| data[].promotional_banner_brands[].promotionalBannerId | integer | No | Not provided | Identifier of the brand banner assigned to the restaurant. |
| data[].promotional_banner_brands[].restaurantId | integer | No | Not provided | Identifier of the target restaurant. |
| data[].promotional_banner_brands[].isActive | boolean | No | Not provided | Activation state for this target restaurant. |
| data[].promotional_banner_brands[].created_at | string | null | No | Not provided | ISO 8601 timestamp at which the target row was created. |
| data[].promotional_banner_brands[].updated_at | string | null | No | Not provided | ISO 8601 timestamp at which the target row was last updated. |
POST/promotional_banners/restaurant/{restaurantId}Create a local banner
Requires Bearer authentication and an Admin, Brand, or Restaurant role. The JSON body is required. Unknown fields are removed by Joi. The defaults are isActive=false, linkType=none, linkTargetId=null, channelId=1, and placement=restaurants.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| restaurantId | integer | Yes | Restaurant that will own the banner (path parameter). |
Request Body
{
"name": "Lunch special",
"imageId": 501,
"isActive": true,
"linkType": "product",
"linkTargetId": 5001,
"channelId": 1,
"placement": "restaurants"
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| name | string | Yes | "Lunch special" | Banner name, with a maximum length of 255 characters. |
| imageId | integer | Yes | 501 | Identifier of an uploaded image to display. |
| isActive | boolean | No | true | Initial activation state. Defaults to false. |
| linkType | string | No | "product" | Click behavior: `none`, `category`, or `product`. Defaults to `none`. |
| linkTargetId | integer | null | No | 5001 | Required when linkType is `category` or `product`; omitted or null when linkType is `none` (the default is null). |
| channelId | integer | No | 1 | Display channel: `1` KIOSK or `2` WEB/CEL. Defaults to `1`. |
| placement | string | No | "restaurants" | Placement: `restaurants` or `foodcourt_home`. Defaults to `restaurants`. |
Response
{
"status": 201,
"code": "create_promotional_banner_succeed",
"message": "Promotional banner created successfully",
"data": {
"promotionalBannerId": 901,
"name": "Lunch special",
"imageId": 501,
"isActive": true,
"linkType": "product",
"linkTargetId": 5001,
"linkTargetRef": null,
"channelId": 1,
"placement": "restaurants",
"restaurantId": 42,
"brandId": null,
"created_at": "2026-07-11T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 501,
"fullUrl": "https://cdn.example.com/promotional-banners/lunch-special.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 201 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "create_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banner created successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Created local banner. |
| data.promotionalBannerId | integer | Yes | 901 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Lunch special" | Human-readable banner name. |
| data.imageId | integer | Yes | 501 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | true | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | 5001 | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | null | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | 42 | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | null | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-11T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 501 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/lunch-special.jpg" | Absolute URL of the resolved banner image. |
PUT/promotional_banners/{promotionalBannerId}Update a local banner
Requires Bearer authentication and an Admin, Brand, or Restaurant role. A JSON object is required but every documented member is optional. This endpoint only accepts local banners; attempting to update a brand banner is denied.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| promotionalBannerId | integer | Yes | Local banner identifier (path parameter). |
Request Body
{
"name": "Lunch special extended",
"imageId": 503,
"linkType": "category",
"linkTargetId": 210
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| name | string | No | "Lunch special extended" | Replacement name, with a maximum length of 255 characters. |
| imageId | integer | No | 503 | Replacement image identifier. |
| linkType | string | No | "category" | Replacement click behavior: `none`, `category`, or `product`. |
| linkTargetId | integer | null | No | 210 | Required if this body sets linkType to `category` or `product`. Otherwise optional and may be null; the service rejects null for an effective non-`none` link. |
Response
{
"status": 200,
"code": "update_promotional_banner_succeed",
"message": "Promotional banner updated successfully",
"data": {
"promotionalBannerId": 901,
"name": "Lunch special extended",
"imageId": 503,
"isActive": true,
"linkType": "category",
"linkTargetId": 210,
"linkTargetRef": null,
"channelId": 1,
"placement": "restaurants",
"restaurantId": 42,
"brandId": null,
"created_at": "2026-07-11T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 501,
"fullUrl": "https://cdn.example.com/promotional-banners/lunch-special.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "update_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banner updated successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Updated local banner. |
| data.promotionalBannerId | integer | Yes | 901 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Lunch special extended" | Human-readable banner name. |
| data.imageId | integer | Yes | 503 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | true | Banner-level activation state. |
| data.linkType | string | Yes | "category" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | 210 | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | null | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | 42 | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | null | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-11T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 501 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/lunch-special.jpg" | Absolute URL of the resolved banner image. |
PATCH/promotional_banners/{promotionalBannerId}/toggleToggle a local banner
Requires Bearer authentication and an Admin, Brand, or Restaurant role. This endpoint only toggles a local banner, not a brand banner.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| promotionalBannerId | integer | Yes | Local banner identifier (path parameter). |
Request Body
{
"isActive": false
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| isActive | boolean | Yes | false | Requested local-banner activation state. |
Response
{
"status": 200,
"code": "toggle_promotional_banner_succeed",
"message": "Promotional banner toggled successfully",
"data": {
"promotionalBannerId": 901,
"name": "Lunch special",
"imageId": 501,
"isActive": false,
"linkType": "product",
"linkTargetId": 5001,
"linkTargetRef": null,
"channelId": 1,
"placement": "restaurants",
"restaurantId": 42,
"brandId": null,
"created_at": "2026-07-11T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 501,
"fullUrl": "https://cdn.example.com/promotional-banners/lunch-special.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "toggle_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banner toggled successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Local banner after toggling. |
| data.promotionalBannerId | integer | Yes | 901 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Lunch special" | Human-readable banner name. |
| data.imageId | integer | Yes | 501 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | false | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | 5001 | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | null | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | 42 | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | null | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-11T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 501 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/lunch-special.jpg" | Absolute URL of the resolved banner image. |
PATCH/promotional_banners/{promotionalBannerId}/restaurant/{restaurantId}/toggleToggle a brand banner for one restaurant
Requires Bearer authentication and an Admin, Brand, or Restaurant role. The banner must be brand-scoped and already targeted to the requested restaurant. The returned record is the global brand banner; it does not include the per-restaurant junction-row activation just changed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| promotionalBannerId | integer | Yes | Brand banner identifier (path parameter). |
| restaurantId | integer | Yes | Target restaurant identifier (path parameter). |
Request Body
{
"isActive": false
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| isActive | boolean | Yes | false | Requested activation state for this restaurant target. |
Response
{
"status": 200,
"code": "toggle_restaurant_brand_banner_succeed",
"message": "Restaurant brand banner toggled successfully",
"data": {
"promotionalBannerId": 902,
"name": "Brand summer offer",
"imageId": 502,
"isActive": true,
"linkType": "product",
"linkTargetId": null,
"linkTargetRef": "SUMMER-BURGER",
"channelId": 1,
"placement": "restaurants",
"restaurantId": null,
"brandId": 7,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 502,
"fullUrl": "https://cdn.example.com/promotional-banners/brand-summer.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "toggle_restaurant_brand_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Restaurant brand banner toggled successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Global brand banner record after the target-row update. |
| data.promotionalBannerId | integer | Yes | 902 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Brand summer offer" | Human-readable banner name. |
| data.imageId | integer | Yes | 502 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | true | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | null | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | "SUMMER-BURGER" | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | null | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | 7 | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-10T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 502 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/brand-summer.jpg" | Absolute URL of the resolved banner image. |
POST/promotional_banners/{promotionalBannerId}/duplicateDuplicate a banner into a restaurant
Requires Bearer authentication and an Admin, Brand, or Restaurant role. Creates an inactive local copy in the supplied restaurant. Food-court-home banners cannot be duplicated. A brand product or category link is resolved against the target restaurant’s active menu and can become `none` if no equivalent target exists.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| promotionalBannerId | integer | Yes | Source banner identifier (path parameter). |
Request Body
{
"restaurantId": 42
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| restaurantId | integer | Yes | 42 | Restaurant that will own the newly created local copy. |
Response
{
"status": 201,
"code": "duplicate_promotional_banner_succeed",
"message": "Promotional banner duplicated",
"data": {
"promotionalBannerId": 903,
"name": "Copie de Brand summer offer",
"imageId": 501,
"isActive": false,
"linkType": "product",
"linkTargetId": 5001,
"linkTargetRef": null,
"channelId": 1,
"placement": "restaurants",
"restaurantId": 42,
"brandId": null,
"created_at": "2026-07-11T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 501,
"fullUrl": "https://cdn.example.com/promotional-banners/lunch-special.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 201 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "duplicate_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banner duplicated" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | New inactive local-banner copy. |
| data.promotionalBannerId | integer | Yes | 903 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Copie de Brand summer offer" | Human-readable banner name. |
| data.imageId | integer | Yes | 501 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | false | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | 5001 | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | null | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | 42 | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | null | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-11T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 501 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/lunch-special.jpg" | Absolute URL of the resolved banner image. |
DELETE/promotional_banners/{promotionalBannerId}Delete a local banner
Requires Bearer authentication and an Admin, Brand, or Restaurant role. This endpoint soft-deletes a local banner. It returns HTTP 204, so Express sends no JSON envelope or response body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| promotionalBannerId | integer | Yes | Local banner identifier (path parameter). |
Response
HTTP 204 No Content
The server deliberately returns no response body.Active display feeds
These guest-accessible endpoints return only the banner fields needed by a display client. They do not return the management representation above.
GET/promotional_banners/restaurant/{restaurantId}/activeGet active banners for a restaurant
Guest-accessible: no bearer token is required. Returns active local banners and active brand banners targeted to the restaurant for the requested channel. Product-linked banners whose products are unavailable on that channel are omitted. channelId defaults to 1 (KIOSK).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| restaurantId | integer | Yes | Restaurant identifier (path parameter). |
| channelId | integer | No | Optional query parameter: `1` for KIOSK or `2` for WEB/CEL. Defaults to `1`. |
Response
{
"status": 200,
"code": "list_promotional_banners_succeed",
"message": "Promotional banners fetched successfully",
"data": {
"banners": [
{
"promotionalBannerId": 901,
"imageUrl": "https://cdn.example.com/promotional-banners/lunch-special.jpg",
"linkType": "product",
"linkTargetId": 5001,
"placement": "restaurants"
}
],
"carouselDelayMs": 4000
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_promotional_banners_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banners fetched successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Active display feed for the requested restaurant and channel. |
| data.banners | array | Yes | […] | Active, channel-eligible banners to display in carousel order. |
| data.banners[] | object | Yes | {…} | One active banner display item. |
| data.banners[].promotionalBannerId | integer | Yes | 901 | Unique identifier of the active banner. |
| data.banners[].imageUrl | string | null | Yes | "https://cdn.example.com/promotional-banners/lunch-special.jpg" | Absolute URL of the banner image, or null when the image cannot be resolved. |
| data.banners[].linkType | string | Yes | "product" | Resolved click behavior: `none`, `category`, or `product`. |
| data.banners[].linkTargetId | integer | null | Yes | 5001 | Resolved category or product ID in this restaurant’s active menu, or null when the banner is not linked. |
| data.banners[].placement | string | Yes | "restaurants" | Stored placement value: `restaurants` or `foodcourt_home`. |
| data.carouselDelayMs | integer | Yes | 4000 | Carousel delay in milliseconds. Defaults to 4000 when no brand value is configured. |
GET/promotional_banners/brand/{brandId}/active/foodcourtGet active food-court banners for a brand
Guest-accessible: no bearer token is required. Returns only active WEB (CEL) brand banners whose placement is foodcourt_home. Product targets that no longer belong to the brand or are not available on WEB are omitted.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
Response
{
"status": 200,
"code": "list_promotional_banners_succeed",
"message": "Promotional banners fetched successfully",
"data": {
"banners": [
{
"promotionalBannerId": 904,
"imageUrl": "https://cdn.example.com/promotional-banners/foodcourt.jpg",
"linkType": "product",
"linkTargetId": 5001,
"linkTargetRef": "5001",
"linkTargetRestaurantId": 42,
"placement": "foodcourt_home"
}
],
"carouselDelayMs": 5000
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_promotional_banners_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banners fetched successfully" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Active food-court display feed for the brand. |
| data.banners | array | Yes | […] | Active food-court-home banners to display. |
| data.banners[] | object | Yes | {…} | One active food-court banner display item. |
| data.banners[].promotionalBannerId | integer | Yes | 904 | Unique identifier of the active food-court banner. |
| data.banners[].imageUrl | string | null | Yes | "https://cdn.example.com/promotional-banners/foodcourt.jpg" | Absolute URL of the banner image, or null when the image cannot be resolved. |
| data.banners[].linkType | string | Yes | "product" | Stored click behavior. New food-court banners may use `none` or `product`; a legacy `category` value is returned unchanged. |
| data.banners[].linkTargetId | integer | null | Yes | 5001 | Resolved product ID, or null when the banner has no product link. |
| data.banners[].linkTargetRef | string | null | Yes | "5001" | Stored brand-level product reference, normally a decimal product-ID string for food-court banners. |
| data.banners[].linkTargetRestaurantId | integer | null | Yes | 42 | Restaurant that owns the resolved linked product, or null when there is no product link. |
| data.banners[].placement | string | Yes | "foodcourt_home" | Always `foodcourt_home` in this endpoint’s results. |
| data.carouselDelayMs | integer | Yes | 5000 | Carousel delay in milliseconds. Defaults to 4000 when no brand value is configured. |
Brand banner management
Brand banners are created once and can be targeted to multiple restaurants. Brand product links use a SKU so the same banner can resolve to a product in each restaurant’s menu; category links use a category name. Food-court-home banners have no restaurant targets.
GET/brands/{brandId}/promotional_bannersList brand banners
Requires Bearer authentication and an Admin or Brand role. Returns each brand banner with its target rows and aggregate active/total target counts.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
| sort | string | No | Optional query parameter. Use `name`, `status`, `linkType`, or `created_at`, as `field:asc`, `field:desc`, `-field`, or a comma-separated combination. Unknown fields are ignored; default order is created_at ascending. |
Response
{
"status": 200,
"code": "list_brand_promotional_banners_succeed",
"message": "Brand promotional banners fetched",
"data": [
{
"promotionalBannerId": 902,
"name": "Brand summer offer",
"imageId": 502,
"isActive": true,
"linkType": "product",
"linkTargetId": null,
"linkTargetRef": "SUMMER-BURGER",
"channelId": 1,
"placement": "restaurants",
"restaurantId": null,
"brandId": 7,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 502,
"fullUrl": "https://cdn.example.com/promotional-banners/brand-summer.jpg"
},
"promotional_banner_brands": [
{
"promotionalBannerId": 902,
"restaurantId": 42,
"isActive": true,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z"
},
{
"promotionalBannerId": 902,
"restaurantId": 43,
"isActive": false,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z"
}
],
"activationCount": 1,
"targetCount": 2
}
]
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_brand_promotional_banners_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand promotional banners fetched" | Localized human-readable success message. Do not use this value for program logic. |
| data | array | Yes | […] | Brand banner records including target rows and counts. |
| data[] | object | Not specified | {…} | Endpoint-specific response payload. |
| data[].promotionalBannerId | integer | Yes | 902 | Unique identifier of the promotional banner. |
| data[].name | string | Yes | "Brand summer offer" | Human-readable banner name. |
| data[].imageId | integer | Yes | 502 | Identifier of the image selected for the banner. |
| data[].isActive | boolean | Yes | true | Banner-level activation state. |
| data[].linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data[].linkTargetId | integer | null | Yes | null | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data[].linkTargetRef | string | null | Yes | "SUMMER-BURGER" | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data[].channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data[].placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data[].restaurantId | integer | null | Yes | null | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data[].brandId | integer | null | Yes | 7 | Owning brand for a brand-scoped banner; null for a local banner. |
| data[].created_at | string | Yes | "2026-07-10T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data[].updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data[].deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data[].image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data[].image.imageId | integer | No | 502 | Identifier of the resolved image. |
| data[].image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/brand-summer.jpg" | Absolute URL of the resolved banner image. |
| data[].promotional_banner_brands | array | Yes | […] | Per-restaurant target rows. This property is present for brand banners in list responses, not local banners. |
| data[].promotional_banner_brands[] | object | Yes | {…} | One brand-banner assignment to a restaurant. |
| data[].promotional_banner_brands[].promotionalBannerId | integer | Yes | 902 | Identifier of the brand banner assigned to the restaurant. |
| data[].promotional_banner_brands[].restaurantId | integer | Yes | 42 | Identifier of the target restaurant. |
| data[].promotional_banner_brands[].isActive | boolean | Yes | true | Activation state for this target restaurant. |
| data[].promotional_banner_brands[].created_at | string | null | Yes | "2026-07-10T09:00:00.000Z" | ISO 8601 timestamp at which the target row was created. |
| data[].promotional_banner_brands[].updated_at | string | null | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the target row was last updated. |
| data[].activationCount | integer | Yes | 1 | Number of target restaurants where this banner is currently active. |
| data[].targetCount | integer | Yes | 2 | Number of target restaurants attached to this banner, active or inactive. |
POST/brands/{brandId}/promotional_bannersCreate a brand banner
Requires Bearer authentication and an Admin or Brand role. The JSON body is required and unknown fields are removed by Joi. For placement=restaurants, an active banner needs at least one eligible target restaurant owned by the brand. For placement=foodcourt_home, restaurantIds is ignored and linkType=category is rejected by the service.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand that will own the banner (path parameter). |
Request Body
{
"name": "Brand summer offer",
"imageId": 502,
"isActive": true,
"linkType": "product",
"linkTargetRef": "SUMMER-BURGER",
"channelId": 1,
"placement": "restaurants",
"restaurantIds": [
42,
43
]
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| name | string | Yes | "Brand summer offer" | Banner name, with a maximum length of 255 characters. |
| imageId | integer | Yes | 502 | Identifier of an uploaded image to display. |
| isActive | boolean | No | true | Initial global activation state. Defaults to false. |
| linkType | string | No | "product" | Click behavior: `none`, `category`, or `product`. Defaults to `none`. |
| linkTargetRef | string | null | No | "SUMMER-BURGER" | Required when linkType is `category` or `product`; otherwise optional, nullable, and defaults to null. Use a category name for category links and a SKU for restaurant-placement product links. A food-court product link must use a decimal product-ID string. |
| channelId | integer | No | 1 | Display channel: `1` KIOSK or `2` WEB/CEL. Defaults to `1`. |
| placement | string | No | "restaurants" | Placement: `restaurants` or `foodcourt_home`. Defaults to `restaurants`. Only WEB food-court banners are returned by the public food-court feed. |
| restaurantIds | array | null | No | […] | Target restaurant IDs. Defaults to an empty array and is ignored for foodcourt_home placement. |
| restaurantIds[] | integer | No | 42 | One target restaurant identifier, which must belong to the brand. |
Response
{
"status": 201,
"code": "create_brand_promotional_banner_succeed",
"message": "Brand promotional banner created",
"data": {
"promotionalBannerId": 902,
"name": "Brand summer offer",
"imageId": 502,
"isActive": true,
"linkType": "product",
"linkTargetId": null,
"linkTargetRef": "SUMMER-BURGER",
"channelId": 1,
"placement": "restaurants",
"restaurantId": null,
"brandId": 7,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 502,
"fullUrl": "https://cdn.example.com/promotional-banners/brand-summer.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 201 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "create_brand_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand promotional banner created" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Created brand banner. Target rows are not included in this response. |
| data.promotionalBannerId | integer | Yes | 902 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Brand summer offer" | Human-readable banner name. |
| data.imageId | integer | Yes | 502 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | true | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | null | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | "SUMMER-BURGER" | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | null | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | 7 | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-10T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 502 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/brand-summer.jpg" | Absolute URL of the resolved banner image. |
PUT/brands/{brandId}/promotional_banners/{promotionalBannerId}Update a brand banner
Requires Bearer authentication and an Admin or Brand role. A JSON object is required, but all its documented members are optional. `restaurantIdsToAdd` and `restaurantIdsToRemove` default to empty arrays. When placement becomes foodcourt_home, target changes are ignored and existing restaurant targets are detached; foodcourt_home cannot use linkType=category.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier in the route (path parameter). Access is checked against the banner owner. |
| promotionalBannerId | integer | Yes | Brand banner identifier (path parameter). |
Request Body
{
"name": "Brand summer offer extended",
"imageId": 504,
"linkType": "product",
"linkTargetRef": "SUMMER-BURGER-XL",
"placement": "restaurants",
"restaurantIdsToAdd": [
44
],
"restaurantIdsToRemove": [
43
]
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| name | string | No | "Brand summer offer extended" | Replacement name, with a maximum length of 255 characters. |
| imageId | integer | No | 504 | Replacement image identifier. |
| linkType | string | No | "product" | Replacement click behavior: `none`, `category`, or `product`. `category` is invalid with foodcourt_home. |
| linkTargetRef | string | null | No | "SUMMER-BURGER-XL" | Replacement reusable target reference, up to 255 characters; null is accepted. The schema does not make this field conditionally required for a partial update. |
| placement | string | No | "restaurants" | Replacement placement: `restaurants` or `foodcourt_home`. |
| restaurantIdsToAdd | array | No | […] | Restaurant target IDs to attach. Defaults to []; ignored for foodcourt_home. Each added restaurant must belong to the brand. |
| restaurantIdsToAdd[] | integer | No | 44 | One restaurant target to attach. |
| restaurantIdsToRemove | array | No | […] | Restaurant target IDs to detach. Defaults to []; ignored for foodcourt_home. |
| restaurantIdsToRemove[] | integer | No | 43 | One restaurant target to detach. |
Response
{
"status": 200,
"code": "update_brand_promotional_banner_succeed",
"message": "Brand promotional banner updated",
"data": {
"promotionalBannerId": 902,
"name": "Brand summer offer extended",
"imageId": 504,
"isActive": true,
"linkType": "product",
"linkTargetId": null,
"linkTargetRef": "SUMMER-BURGER-XL",
"channelId": 1,
"placement": "restaurants",
"restaurantId": null,
"brandId": 7,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 502,
"fullUrl": "https://cdn.example.com/promotional-banners/brand-summer.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "update_brand_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand promotional banner updated" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Updated brand banner. Target rows are not included in this response. |
| data.promotionalBannerId | integer | Yes | 902 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Brand summer offer extended" | Human-readable banner name. |
| data.imageId | integer | Yes | 504 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | true | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | null | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | "SUMMER-BURGER-XL" | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | null | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | 7 | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-10T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 502 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/brand-summer.jpg" | Absolute URL of the resolved banner image. |
PATCH/brands/{brandId}/promotional_banners/{promotionalBannerId}/toggleToggle a brand banner globally
Requires Bearer authentication and an Admin or Brand role. For restaurants placement, toggling also sets the same activation state on every target row. Activating a restaurants-placement banner with no targets is rejected. Food-court-home banners have no target rows and use only their global isActive state.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier in the route (path parameter). Access is checked against the banner owner. |
| promotionalBannerId | integer | Yes | Brand banner identifier (path parameter). |
Request Body
{
"isActive": false
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| isActive | boolean | Yes | false | Requested global activation state. |
Response
{
"status": 200,
"code": "toggle_brand_promotional_banner_succeed",
"message": "Brand promotional banner toggled",
"data": {
"promotionalBannerId": 902,
"name": "Brand summer offer",
"imageId": 502,
"isActive": false,
"linkType": "product",
"linkTargetId": null,
"linkTargetRef": "SUMMER-BURGER",
"channelId": 1,
"placement": "restaurants",
"restaurantId": null,
"brandId": 7,
"created_at": "2026-07-10T09:00:00.000Z",
"updated_at": "2026-07-11T09:15:00.000Z",
"deleted_at": null,
"image": {
"imageId": 502,
"fullUrl": "https://cdn.example.com/promotional-banners/brand-summer.jpg"
}
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "toggle_brand_promotional_banner_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand promotional banner toggled" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Brand banner after global toggle. |
| data.promotionalBannerId | integer | Yes | 902 | Unique identifier of the promotional banner. |
| data.name | string | Yes | "Brand summer offer" | Human-readable banner name. |
| data.imageId | integer | Yes | 502 | Identifier of the image selected for the banner. |
| data.isActive | boolean | Yes | false | Banner-level activation state. |
| data.linkType | string | Yes | "product" | Link behavior: `none`, `category`, or `product`. |
| data.linkTargetId | integer | null | Yes | null | Local category/product target ID when known. Brand banners normally use linkTargetRef instead. |
| data.linkTargetRef | string | null | Yes | "SUMMER-BURGER" | Brand-level target reference (SKU for products or category name for categories), or null when unused. |
| data.channelId | integer | Yes | 1 | Display channel: `1` for KIOSK and `2` for WEB (CEL). |
| data.placement | string | Yes | "restaurants" | Placement: `restaurants` or `foodcourt_home`. |
| data.restaurantId | integer | null | Yes | null | Owning restaurant for a local banner; null for a brand-scoped banner. |
| data.brandId | integer | null | Yes | 7 | Owning brand for a brand-scoped banner; null for a local banner. |
| data.created_at | string | Yes | "2026-07-10T09:00:00.000Z" | ISO 8601 timestamp at which the banner was created. |
| data.updated_at | string | Yes | "2026-07-11T09:15:00.000Z" | ISO 8601 timestamp at which the banner was last updated. |
| data.deleted_at | string | null | Yes | null | Soft-deletion timestamp. It is null for records returned by these endpoints. |
| data.image | object | null | Yes | {…} | Resolved image record, when the referenced image still exists. |
| data.image.imageId | integer | No | 502 | Identifier of the resolved image. |
| data.image.fullUrl | string | null | No | "https://cdn.example.com/promotional-banners/brand-summer.jpg" | Absolute URL of the resolved banner image. |
DELETE/brands/{brandId}/promotional_banners/{promotionalBannerId}Delete a brand banner
Requires Bearer authentication and an Admin or Brand role. Deletes the banner and all of its restaurant target rows. It returns HTTP 204, so Express sends no JSON envelope or response body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier in the route (path parameter). Access is checked against the banner owner. |
| promotionalBannerId | integer | Yes | Brand banner identifier (path parameter). |
Response
HTTP 204 No Content
The server deliberately returns no response body.Brand settings and selection data
These authenticated brand endpoints support carousel configuration and user interfaces that choose banner targets. The pickup-schedule endpoint lives with the promotional-banner controllers because the food-court configuration uses the brand’s restaurant pickup availability.
GET/brands/{brandId}/promotional_banners_settingsGet brand carousel settings
Requires Bearer authentication and an Admin or Brand role. Returns the shared carousel delay used by both KIOSK and WEB banner feeds. It defaults to 4000 milliseconds when the brand has no configured value.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
Response
{
"status": 200,
"code": "get_brand_promotional_banners_settings_succeed",
"message": "Settings fetched",
"data": {
"carouselDelayMs": 4000
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "get_brand_promotional_banners_settings_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Settings fetched" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Brand carousel configuration. |
| data.carouselDelayMs | integer | Yes | 4000 | Carousel delay in milliseconds; 4000 is the service default. |
PUT/brands/{brandId}/promotional_banners_settingsUpdate brand carousel settings
Requires Bearer authentication and an Admin or Brand role. The JSON body is required. Changing the delay reloads the brand’s restaurants; sending the existing value leaves it unchanged.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
Request Body
{
"carouselDelayMs": 5000
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| carouselDelayMs | integer | Yes | 5000 | Carousel delay in milliseconds. Must be an integer greater than or equal to 1. |
Response
{
"status": 200,
"code": "update_brand_promotional_banners_settings_succeed",
"message": "Settings updated",
"data": {
"carouselDelayMs": 5000
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "update_brand_promotional_banners_settings_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Settings updated" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Updated brand carousel configuration. |
| data.carouselDelayMs | integer | Yes | 5000 | Persisted carousel delay in milliseconds. |
GET/brands/{brandId}/promotional_banner_targetsList eligible brand-banner restaurants
Requires Bearer authentication and an Admin or Brand role. Returns every restaurant owned by the brand and independently reports whether it has each display channel and an active menu on that channel.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
Response
{
"status": 200,
"code": "list_brand_promotional_banner_targets_succeed",
"message": "Brand promotional banner targets fetched",
"data": [
{
"restaurantId": 42,
"name": "Central restaurant",
"hasKioskChannel": true,
"hasActiveKioskMenu": true,
"hasCelChannel": true,
"hasActiveCelMenu": false
}
]
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_brand_promotional_banner_targets_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand promotional banner targets fetched" | Localized human-readable success message. Do not use this value for program logic. |
| data | array | Yes | […] | Restaurants available as brand-banner targets. |
| data[] | object | Yes | {…} | One restaurant owned by the brand. |
| data[].restaurantId | integer | Yes | 42 | Restaurant identifier. |
| data[].name | string | Yes | "Central restaurant" | Restaurant name; it can be an empty string when no name is stored. |
| data[].hasKioskChannel | boolean | Yes | true | Whether the restaurant has the KIOSK channel configured. |
| data[].hasActiveKioskMenu | boolean | Yes | true | Whether the restaurant has an active KIOSK menu. |
| data[].hasCelChannel | boolean | Yes | true | Whether the restaurant has the WEB/CEL channel configured. |
| data[].hasActiveCelMenu | boolean | Yes | false | Whether the restaurant has an active WEB/CEL menu. |
GET/brands/{brandId}/promotional_banner_targets/cel_productsList CEL products across a brand
Requires Bearer authentication and an Admin or Brand role. Aggregates products from every active WEB/CEL menu of the brand’s restaurants. This is the product picker for food-court-home banner links.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
Response
{
"status": 200,
"code": "list_brand_promotional_banner_cel_products_succeed",
"message": "Brand CEL products fetched",
"data": [
{
"restaurantId": 42,
"restaurantName": "Central restaurant",
"productId": 5001,
"productName": "Classic burger",
"sku": "SUMMER-BURGER"
}
]
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_brand_promotional_banner_cel_products_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand CEL products fetched" | Localized human-readable success message. Do not use this value for program logic. |
| data | array | Yes | […] | Products from active brand WEB/CEL menus. |
| data[] | object | Yes | {…} | One product available in an active WEB/CEL menu. |
| data[].restaurantId | integer | Yes | 42 | Restaurant that owns the active menu containing this product. |
| data[].restaurantName | string | Yes | "Central restaurant" | Name of that restaurant; it can be an empty string when no name is stored. |
| data[].productId | integer | Yes | 5001 | Product identifier. |
| data[].productName | string | Yes | "Classic burger" | Product display name. |
| data[].sku | string | null | Yes | "SUMMER-BURGER" | Product SKU, or null when the product has no SKU. |
GET/brands/{brandId}/pickup_schedulesList brand restaurant pickup schedules
Requires Bearer authentication and an Admin or Brand role. Returns all pickup schedules grouped by restaurant ID. Every restaurant owned by the brand is present as a JSON object key, including those with no schedules.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| brandId | integer | Yes | Brand identifier (path parameter). |
Response
{
"status": 200,
"code": "list_brand_pickup_schedules_succeed",
"message": "Brand pickup schedules fetched",
"data": {
"42": [
{
"pickupScheduleId": 801,
"restaurantId": 42,
"day": "MON",
"start": 660,
"end": 840
}
],
"43": []
}
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 200 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "list_brand_pickup_schedules_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Brand pickup schedules fetched" | Localized human-readable success message. Do not use this value for program logic. |
| data | object | Yes | {…} | Object keyed by restaurant ID (JSON object keys are strings). |
| data.42 | array | Yes | […] | Example schedule array for restaurant 42. In production, replace `42` with any restaurant ID owned by the requested brand. |
| data.42[] | object | Yes | {…} | One pickup schedule for the restaurant represented by the object key. |
| data.42[].pickupScheduleId | integer | Yes | 801 | Pickup schedule identifier. |
| data.42[].restaurantId | integer | Yes | 42 | Restaurant identifier; it matches the enclosing object key. |
| data.42[].day | string | Yes | "MON" | Weekday code: MON, TUE, WED, THU, FRI, SAT, or SUN. |
| data.42[].start | integer | Yes | 660 | Start minute after midnight (0–1440). Example 660 is 11:00. |
| data.42[].end | integer | Yes | 840 | End minute after midnight (0–1440). Example 840 is 14:00. |
| data.43 | array | Yes | [] | Example empty schedule array for a brand restaurant with no pickup schedules. |
Engagement tracking
The tracking endpoint accepts interaction events from CEL and Borne. Events are best-effort: once validation succeeds, a Pub/Sub publish failure is logged internally and does not turn the request into an error response.
POST/tracking/promotional_banner_eventTrack a promotional-banner event
Guest-accessible: no bearer token is required. The JSON body is required and unknown fields are stripped by Joi. clientTimestamp must be an ISO date string. productId is required only for product_added_post_banner and is optional for promotional_banner_clicked.
Request Body
{
"eventName": "product_added_post_banner",
"bannerId": 904,
"restaurantId": 42,
"canal": "CEL",
"clientTimestamp": "2026-07-11T10:30:00.000Z",
"productId": 5001
}Request Body Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| eventName | string | Yes | "product_added_post_banner" | Event type: `promotional_banner_clicked` or `product_added_post_banner`. |
| bannerId | integer | Yes | 904 | Identifier of the banner that led to the interaction. |
| restaurantId | integer | Yes | 42 | Restaurant context in which the event occurred. |
| canal | string | Yes | "CEL" | Client channel: `CEL` for web ordering or `BORNE` for kiosk. |
| clientTimestamp | string | Yes | "2026-07-11T10:30:00.000Z" | ISO 8601 timestamp generated by the client. |
| productId | integer | No | 5001 | Required when eventName is `product_added_post_banner`; optional for `promotional_banner_clicked`. |
Response
{
"status": 202,
"code": "track_promotional_banner_event_succeed",
"message": "Promotional banner event tracked",
"data": null
}Response Properties
Every field in the example is listed below. Explicit requiredness is shown when the endpoint contract defines it.
| Property | Type | Required | Example | Description |
|---|---|---|---|---|
| status | integer | Yes | 202 | HTTP status code, repeated in the successful JSON envelope. |
| code | string | Yes | "track_promotional_banner_event_succeed" | Stable application success code for this operation. |
| message | string | Yes | "Promotional banner event tracked" | Localized human-readable success message. Do not use this value for program logic. |
| data | null | Yes | null | Always null. The event has been accepted for best-effort asynchronous publishing. |