# Terasoft CMS API — Mobile Integration Guide

> **Note**: This guide is for the **development environment** only.

> **Base URL**: `https://{host}/api/v1/terasoft`
> **Base Path**: `api/v1/terasoft`
> **Version**: v1
>
> **Protocol**: REST (GET only)

---

## Host

DEV: https://cms-ads-proxy.dev.aperogroup.ai
PROD: https://cms-ads-proxy.aperogroup.ai

## Authentication & Headers

| Header           | Required | Description                                                                                                                                                   |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-api-bundleid` | Yes      | App bundle ID (e.g., `com.example.terasoft`)                                                                                                                  |
| `country-code`   | No       | ISO country code (e.g., `VN`, `US`). Resolves localized content and filters by country targeting. If omitted, only default (non-targeted) items are returned. |
| `app-version`    | No       | Semantic version string (e.g., `1.2.0`). Filters by targeting version range. If omitted, only default (non-targeted) items are returned.                      |

---

## Endpoints

### 1. Get App Configuration

Retrieves the full app config including screens, banners, categories, and styles.

```
GET /api/v1/terasoft/app
```

**Example Request**

```bash
curl 'https://{host}/api/v1/terasoft/app' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN' \
  -H 'app-version: 1.2.0'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": {
    "id": 1,
    "documentId": "hc38djwakn6k0nnr32ktm5jg",
    "code": "terasoft-app",
    "name": "Terasoft App",
    "bundleId": "com.example.terasoft",
    "screens": [
      {
        "id": 1,
        "title": "Home",
        "banners": [
          {
            "id": 4,
            "action": "STYLE",
            "tool": null,
            "categoryCode": null,
            "thumbnail": {
              "id": 5,
              "documentId": "awyw2g4dtdr4k86oxcg4t286",
              "ext": ".jpg",
              "mime": "image/jpeg",
              "url": "https://dev-static.apero.vn/content/uploads/banner.jpg",
              "path": "/content/uploads/banner.jpg",
              "width": 640,
              "height": 959
            },
            "style": { "documentId": "sueljb97n89rbw6wf2f1prqk" },
            "title": "Banner VN",
            "subTitle": "Subtitle VN"
          },
          {
            "id": 7,
            "action": "CATEGORY",
            "tool": null,
            "categoryCode": "category.1",
            "thumbnail": {
              "id": 6,
              "documentId": "yojvtj7b23sy7oqkzkk8zk8e",
              "ext": ".jpg",
              "mime": "image/jpeg",
              "url": "https://dev-static.apero.vn/content/uploads/banner2.jpg",
              "path": "/content/uploads/banner2.jpg",
              "width": 640,
              "height": 960
            },
            "style": null,
            "title": "Banner Category VN",
            "subTitle": "Subtitle Category VN"
          },
          {
            "id": 8,
            "action": "STYLE_BLOCK",
            "tool": null,
            "categoryCode": null,
            "thumbnail": {
              "id": 7,
              "documentId": "xyz789abc012def345",
              "ext": ".jpg",
              "mime": "image/jpeg",
              "url": "https://dev-static.apero.vn/content/uploads/banner3.jpg",
              "path": "/content/uploads/banner3.jpg",
              "width": 640,
              "height": 960
            },
            "style": { "documentId": "sb_abc123def456ghijk" },
            "title": "Curated Collection",
            "subTitle": "Hand-picked styles"
          }
        ],
        "categories": [
          {
            "id": 1,
            "code": "1",
            "title": "Category VN",
            "subTitle": "Subtitle",
            "layout": null,
            "tags": ["NEW"],
            "cardPreviews": [
              {
                "id": 16,
                "role": "BEFORE",
                "media": {
                  "id": 6,
                  "documentId": "yojvtj7b23sy7oqkzkk8zk8e",
                  "ext": ".jpg",
                  "mime": "image/jpeg",
                  "url": "https://dev-static.apero.vn/content/uploads/preview.jpg",
                  "path": "/content/uploads/preview.jpg",
                  "width": 640,
                  "height": 960
                }
              }
            ],
            "styles": [
              {
                "title": null,
                "subTitle": null,
                "tags": ["NEW"],
                "layout": null,
                "promptParams": { "color": "red" },
                "cardPreviews": [],
                "examples": [
                  { "id": 1, "type": "GOOD", "description": "Clear portrait, natural lighting", "thumbnail": { "id": 20, "documentId": "f020", "ext": ".jpg", "mime": "image/jpeg", "url": "/uploads/good.jpg", "path": "/uploads/good.jpg", "width": 640, "height": 960 } },
                  { "id": 2, "type": "BAD", "description": "Blurry, poor composition", "thumbnail": null }
                ],
                "usecases": [
                  { "code": "portrait", "name": "Portrait" }
                ],
                "documentId": "sueljb97n89rbw6wf2f1prqk",
                "code": "terasoft-style-example",
                "serviceUrl": "https://service.example.com",
                "configuration": {},
                "adsCount": 1,
                "monetization": "FREE",
                "workflowId": "workflow-anime-v1",
                "isUserPromptRequired": false,
                "requiredImageCount": 1,
                "styleType": "IMAGE"
              }
            ]
          }
        ]
      }
    ]
  },
  "timestamp": "10/03/2026 10:30:00",
  "path": "/api/v1/terasoft/app",
  "traceId": "8d8a89f5-5913-42e6-916d-1abd8b612a65"
}
```

**Response Data Model**

| Field      | Type     | Description           |
| ---------- | -------- | --------------------- |
| `code`     | string   | App code              |
| `name`     | string   | App display name      |
| `bundleId` | string   | App bundle identifier |
| `screens`  | Screen[] | App screens           |

**Screen**

| Field        | Type       | Description                |
| ------------ | ---------- | -------------------------- |
| `title`      | string     | Screen name (e.g., "Home") |
| `banners`    | Banner[]   | Promotional banners        |
| `categories` | Category[] | Style categories           |

**Banner**

| Field          | Type      | Description                                                                                                                                                                                                    |
| -------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action`       | string    | Banner action type: `STYLE`, `CATEGORY`, `TOOL`, or `STYLE_BLOCK`                                                                                                                                              |
| `tool`         | string?   | Tool identifier (has value when `action=TOOL`, else `null`)                                                                                                                                                    |
| `categoryCode` | string?   | CategoryBlock code (has value when `action=CATEGORY`, else `null`)                                                                                                                                             |
| `thumbnail`    | Media?    | Banner image                                                                                                                                                                                                   |
| `style`        | StyleRef? | Style reference `{ documentId }`. When `action=STYLE`, `documentId` is a collection documentId. When `action=STYLE_BLOCK`, `documentId` is the style block code (e.g., `sb_abc123`). `null` for other actions. |
| `title`        | string?   | Resolved localized title (string or null)                                                                                                                                                                      |
| `subTitle`     | string?   | Resolved localized subtitle (string or null)                                                                                                                                                                   |

**Category**

| Field          | Type          | Description                                                                               |
| -------------- | ------------- | ----------------------------------------------------------------------------------------- |
| `id`           | number        | Entry ID                                                                                  |
| `code`         | string        | Category unique code identifier (e.g., `category.1`)                                      |
| `title`        | string?       | Resolved localized title based on `country-code` header (string or `null`)                |
| `subTitle`     | string?       | Resolved localized subtitle based on `country-code` header (string or `null`)             |
| `layout`       | string?       | Visual layout type for client rendering (e.g., `grid`, `list`). `null` if not configured. |
| `tags`         | string[]      | Display tags: `NEW`, `HOT`, `TRENDING` (multi-select)                                     |
| `cardPreviews` | CardPreview[] | Preview images for this category (used as category thumbnail/cover)                       |
| `styles`       | StyleBlock[]  | Styles belonging to this category                                                         |

**StyleBlock** (flattened — StyleBlock merged with inner style relation)

| Field                  | Type              | Description                                               |
| ---------------------- | ----------------- | --------------------------------------------------------- |
| `title`                | string?           | Resolved title (outer override → inner style fallback)    |
| `subTitle`             | string?           | Resolved subtitle (outer override → inner style fallback) |
| `tags`                 | string[]          | Display tags: `NEW`, `HOT`, `TRENDING` (multi-select)     |
| `layout`               | string?           | Visual layout type (optional)                             |
| `promptParams`         | object?           | JSON prompt parameter configuration (nullable)            |
| `cardPreviews`         | CardPreview[]     | Preview images (outer override → inner style fallback)    |
| `examples`             | Example[]         | Good/bad examples (outer override → inner style fallback) |
| `usecases`             | UsecaseListItem[] | Associated usecases `[{ code, name }]`                    |
| `documentId`           | string?           | Strapi documentId of the referenced style                 |
| `code`                 | string?           | Unique style code                                         |
| `serviceUrl`           | string?           | Backend processing service URL                            |
| `configuration`        | object?           | Opaque JSON config for processing service                 |
| `adsCount`             | number?           | Number of ads to show                                     |
| `monetization`         | string?           | `FREE`, `PAID`, or `SUB_ONLY`                             |
| `workflowId`           | string?           | Workflow identifier for processing                        |
| `isUserPromptRequired` | boolean?          | Whether user prompt is required                           |
| `requiredImageCount`   | number?           | Number of images required                                 |
| `styleType`            | string?           | `IMAGE` or `VIDEO`                                        |

---

### 2. Get Categories

Returns a flat list of categories from the app.

```
GET /api/v1/terasoft/categories
```

**Query Parameters**

| Parameter      | Required | Description                                                              |
| -------------- | -------- | ------------------------------------------------------------------------ |
| `categoryCode` | No       | Filter to a single category by its code                                  |
| `categoryTags` | No       | Filter by category tags (comma-separated: `NEW,HOT,TRENDING`). OR logic. |
| `variant`      | No       | Variant label for A/B testing (e.g., `A`, `B`, `control`)                |

**Example Request**

```bash
curl 'https://{host}/api/v1/terasoft/categories' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN' \
  -H 'app-version: 1.2.0'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": [
    {
      "id": 3,
      "code": "category.1",
      "layout": "grid",
      "tags": ["NEW"],
      "cardPreviews": [
        {
          "id": 25,
          "role": "BEFORE",
          "media": {
            "id": 6,
            "documentId": "yojvtj7b23sy7oqkzkk8zk8e",
            "ext": ".jpg",
            "mime": "image/jpeg",
            "url": "https://dev-static.apero.vn/content/uploads/preview.jpg",
            "path": "/content/uploads/preview.jpg",
            "width": 640,
            "height": 960
          }
        }
      ],
      "title": "title category VN",
      "subTitle": "sub-title category VN"
    }
  ],
  "timestamp": "10/03/2026 15:44:45",
  "path": "/api/v1/terasoft/categories",
  "traceId": "..."
}
```

**CategoryListItem** (same as Category in app response, without `styles`)

| Field          | Type          | Description                                                                               |
| -------------- | ------------- | ----------------------------------------------------------------------------------------- |
| `id`           | number        | Entry ID                                                                                  |
| `code`         | string        | Category unique code identifier (e.g., `category.1`)                                      |
| `title`        | string?       | Resolved localized title based on `country-code` header (string or `null`)                |
| `subTitle`     | string?       | Resolved localized subtitle based on `country-code` header (string or `null`)             |
| `layout`       | string?       | Visual layout type for client rendering (e.g., `grid`, `list`). `null` if not configured. |
| `tags`         | string[]      | Display tags: `NEW`, `HOT`, `TRENDING` (multi-select)                                     |
| `cardPreviews` | CardPreview[] | Preview images for this category (used as category thumbnail/cover)                       |

---

### 3. Get Category Tags

Returns distinct tags present across all visible categories.

```
GET /api/v1/terasoft/category-tags
```

**Query Parameters**

| Parameter | Required | Description                                               |
| --------- | -------- | --------------------------------------------------------- |
| `variant` | No       | Variant label for A/B testing (e.g., `A`, `B`, `control`) |

**Example Request**

```bash
curl 'https://{host}/api/v1/terasoft/category-tags' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN' \
  -H 'app-version: 1.2.0'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": ["NEW", "HOT", "TRENDING"],
  "timestamp": "10/03/2026 10:31:10",
  "path": "/api/v1/terasoft/category-tags",
  "traceId": "..."
}
```

> **Note**: `data` is a flat `string[]` array of unique tag values from visible categories.

---

### 4. Get Tags (style block tags)

Returns unique tags from all styles in the app.

```
GET /api/v1/terasoft/tags
```

**Query Parameters**

| Parameter      | Required | Description                                               |
| -------------- | -------- | --------------------------------------------------------- |
| `categoryCode` | No       | Filter by category code                                   |
| `usecase`      | No       | Filter by usecase code                                    |
| `variant`      | No       | Variant label for A/B testing (e.g., `A`, `B`, `control`) |

**Example Request**

```bash
curl 'https://{host}/api/v1/terasoft/tags?categoryCode=1&usecase=portrait' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN' \
  -H 'app-version: 1.2.0'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": ["NEW", "HOT", "TRENDING"],
  "timestamp": "10/03/2026 10:31:15",
  "path": "/api/v1/terasoft/tags",
  "traceId": "..."
}
```

> **Note**: `data` is a flat `string[]` array of unique tag values.

---

### 5. Get Usecases

Returns unique usecases from all styles in the app.

```
GET /api/v1/terasoft/usecases
```

**Query Parameters**

| Parameter      | Required | Description                                                     |
| -------------- | -------- | --------------------------------------------------------------- |
| `categoryCode` | No       | Filter by category code                                         |
| `tags`         | No       | Filter by tags (comma-separated: `NEW,HOT,TRENDING`). OR logic. |
| `usecase`      | No       | Filter by usecase code                                          |
| `variant`      | No       | Variant label for A/B testing (e.g., `A`, `B`, `control`)       |

**Example Request**

```bash
curl 'https://{host}/api/v1/terasoft/usecases?categoryCode=1&tags=NEW' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN' \
  -H 'app-version: 1.2.0'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": [
    { "code": "portrait", "name": "Portrait" },
    { "code": "landscape", "name": "Landscape" }
  ],
  "timestamp": "10/03/2026 10:31:30",
  "path": "/api/v1/terasoft/usecases",
  "traceId": "..."
}
```

**UsecaseListItem**

| Field  | Type    | Description          |
| ------ | ------- | -------------------- |
| `code` | string  | Usecase code         |
| `name` | string? | Usecase display name |

---

### 6. Get Styles (with filters)

Returns all styles from the app, with optional query filters.

```
GET /api/v1/terasoft/styles
```

**Query Parameters**

| Parameter      | Required | Description                                                     |
| -------------- | -------- | --------------------------------------------------------------- |
| `categoryCode` | No       | Filter by category code                                         |
| `tags`         | No       | Filter by tags (comma-separated: `NEW,HOT,TRENDING`). OR logic. |
| `usecaseCode`  | No       | Filter by usecase code                                          |
| `variant`      | No       | Variant label for A/B testing (e.g., `A`, `B`, `control`)       |

**Example Request**

```bash
curl 'https://{host}/api/v1/terasoft/styles?categoryCode=1&tags=NEW&usecaseCode=portrait' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN' \
  -H 'app-version: 1.2.0'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": [
    {
      "title": null,
      "subTitle": null,
      "tags": ["NEW"],
      "layout": null,
      "promptParams": null,
      "cardPreviews": [],
      "examples": [],
      "usecases": [
        { "code": "portrait", "name": "Portrait" }
      ],
      "documentId": "sueljb97n89rbw6wf2f1prqk",
      "code": "terasoft-style-example",
      "serviceUrl": "https://service.example.com",
      "configuration": {},
      "adsCount": 1,
      "monetization": "FREE",
      "workflowId": "workflow-anime-v1",
      "isUserPromptRequired": false,
      "requiredImageCount": 1,
      "styleType": "IMAGE"
    }
  ],
  "timestamp": "10/03/2026 10:32:00",
  "path": "/api/v1/terasoft/styles",
  "traceId": "..."
}
```

> **Note**: `data` is an array of **StyleBlock** objects (same shape as in app response).

---

### 7. Get Style Detail

Retrieves a single style by its identifier. The identifier can be:
- A **collection documentId** (e.g., `wt2dx8v10ry610d2b8g66yq9`) — fetches from the terasoft-style collection
- A **style block code** (e.g., `sb_abc123def456ghijk`) — fetches the style block with override resolution

Both return the same response shape. Use the `documentId` from `banner.style.documentId` or `category.styles[].documentId`.

```
GET /api/v1/terasoft/styles/{documentId}
```

**Path Parameters**

| Parameter    | Type   | Description                                                         |
| ------------ | ------ | ------------------------------------------------------------------- |
| `documentId` | string | Style collection documentId or style block code (starts with `sb_`) |

**Example Request**

```bash
# Collection style
curl 'https://{host}/api/v1/terasoft/styles/wt2dx8v10ry610d2b8g66yq9' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN'

# Style block
curl 'https://{host}/api/v1/terasoft/styles/sb_abc123def456ghijk' \
  -H 'x-api-bundleid: com.example.terasoft' \
  -H 'country-code: VN'
```

**Example Response** `200 OK`

```json
{
  "statusCode": 200,
  "message": "Success",
  "data": {
    "id": 15,
    "documentId": "wt2dx8v10ry610d2b8g66yq9",
    "code": "terasoft-style-example",
    "styleType": "IMAGE",
    "serviceUrl": "https://service.example.com",
    "configuration": { "prompt": "test" },
    "promptParams": { "color": "red" },
    "workflowId": "workflow-anime-v1",
    "adsCount": 1,
    "monetization": "FREE",
    "isUserPromptRequired": false,
    "requiredImageCount": 1,
    "title": "Example Style",
    "subTitle": "Example Style (sub)",
    "cardPreviews": [
      {
        "id": 14,
        "role": "BEFORE",
        "media": {
          "id": 6,
          "documentId": "yojvtj7b23sy7oqkzkk8zk8e",
          "ext": ".jpg",
          "mime": "image/jpeg",
          "url": "https://dev-static.apero.vn/content/uploads/before.jpg",
          "path": "/content/uploads/before.jpg",
          "width": 640,
          "height": 960
        }
      }
    ],
    "examples": [
      { "id": 1, "type": "GOOD", "description": "Clear portrait", "thumbnail": { "id": 20, "documentId": "f020", "ext": ".jpg", "mime": "image/jpeg", "url": "/uploads/good.jpg", "path": "/uploads/good.jpg", "width": 640, "height": 960 } }
    ]
  },
  "timestamp": "10/03/2026 10:33:00",
  "path": "/api/v1/terasoft/styles/wt2dx8v10ry610d2b8g66yq9",
  "traceId": "0abde934-cce5-4115-bc6d-29e5cfe2da4b"
}
```

**TerasoftStyle Data Model**

| Field                  | Type          | Description                                    |
| ---------------------- | ------------- | ---------------------------------------------- |
| `code`                 | string        | Unique style code                              |
| `styleType`            | string?       | `IMAGE` or `VIDEO`                             |
| `serviceUrl`           | string?       | Backend processing service URL                 |
| `configuration`        | object?       | Opaque JSON config for processing service      |
| `promptParams`         | object?       | JSON prompt parameter configuration (nullable) |
| `workflowId`           | string        | Workflow identifier for processing             |
| `adsCount`             | number?       | Number of ads to show                          |
| `monetization`         | string?       | `FREE`, `PAID`, or `SUB_ONLY`                  |
| `isUserPromptRequired` | boolean?      | Whether user prompt is required                |
| `requiredImageCount`   | number?       | Number of images required                     |
| `title`                | string?       | Resolved localized title                       |
| `subTitle`             | string?       | Resolved localized subtitle                    |
| `cardPreviews`         | CardPreview[] | Preview images                                 |
| `examples`             | Example[]     | Good/bad example images                        |

---

## Shared Types

**StyleRef**

| Field        | Type   | Description        |
| ------------ | ------ | ------------------ |
| `documentId` | string | Strapi document ID |

**Usecase**

| Field        | Type   | Description          |
| ------------ | ------ | -------------------- |
| `id`         | number | Entry ID             |
| `documentId` | string | Strapi document ID   |
| `code`       | string | Usecase code         |
| `name`       | string | Usecase display name |

**CardPreview**

| Field   | Type   | Description                    |
| ------- | ------ | ------------------------------ |
| `id`    | number | Entry ID                       |
| `role`  | string | `BEFORE`, `AFTER`, or `SINGLE` |
| `media` | Media  | Image asset                    |

**Example**

| Field         | Type    | Description                |
| ------------- | ------- | -------------------------- |
| `id`          | number  | Entry ID                   |
| `type`        | string  | `GOOD` or `BAD`            |
| `description` | string? | Description of the example |
| `thumbnail`   | Media?  | Example image (nullable)   |

**Media**

| Field        | Type   | Description                               |
| ------------ | ------ | ----------------------------------------- |
| `id`         | number | Media ID                                  |
| `documentId` | string | Strapi document ID                        |
| `ext`        | string | File extension (e.g., `.jpg`)             |
| `mime`       | string | MIME type (e.g., `image/jpeg`)            |
| `url`        | string | Full CDN URL                              |
| `path`       | string | Path extracted from url (domain stripped) |
| `width`      | number | Image width (px)                          |
| `height`     | number | Image height (px)                         |

---

## Error Responses

All errors follow the same format:

```json
{
  "correlationId": "uuid",
  "statusCode": 404,
  "timestamp": "ISO-8601",
  "path": "/api/v1/terasoft/...",
  "message": "Error description",
  "errorCode": "ERR24"
}
```

| Status | Condition                          |
| ------ | ---------------------------------- |
| `400`  | Missing `x-api-bundleid` header    |
| `404`  | App not found for given `bundleId` |
| `404`  | Style not found for `documentId`   |
| `500`  | Upstream CMS error                 |

---

## Integration Flow

```
1. GET /api/v1/terasoft/app  (with x-api-bundleid + app-version + country-code headers)
   ├── Full app config (screens, banners, categories, style blocks)
   ├── Server returns: default items (no targeting) + items matching your headers
   └── Localized content (title, subTitle) resolved to strings based on country-code

2. GET /api/v1/terasoft/categories  (optional)
   └── Flat list of categories [{ code, title, subTitle, tags }] — targeting applied
       Optional: ?categoryCode=1&categoryTags=NEW,HOT to filter

3. GET /api/v1/terasoft/category-tags  (optional)
   └── Unique tags ["NEW", "HOT", "TRENDING"] from visible categories

4. GET /api/v1/terasoft/tags  (optional)
   └── Unique tags ["NEW", "HOT", "TRENDING"] from visible styles
       Optional: ?categoryCode=1&usecase=portrait to filter

5. GET /api/v1/terasoft/usecases  (optional)
   └── Unique usecases [{ code, name }] from visible styles
       Optional: ?categoryCode=1&tags=NEW to filter

6. GET /api/v1/terasoft/styles?categoryCode=1&tags=NEW&usecaseCode=portrait  (optional)
   └── Filter styles by category, tags (OR logic), and/or usecase — targeting applied

7. GET /api/v1/terasoft/styles/{documentId}
   ├── Full style detail when user selects a style
   ├── Use style.configuration for processing params
   └── No targeting filter — always returns the style if it exists
```

---

## Targeting Behavior

Categories and styles can be assigned **targeting rules** on the CMS (country and/or version range). The server evaluates these rules and only returns items that match the client context. Targeting fields are **not exposed** in the response.

### Rules

| Scenario                                          | Headers sent?                            | Result                      |
| ------------------------------------------------- | ---------------------------------------- | --------------------------- |
| Item has **no targeting** (default item)          | Any                                      | Always included             |
| Item targets country `VN`                         | `country-code: VN`                       | Included                    |
| Item targets country `VN`                         | `country-code: US`                       | Excluded                    |
| Item targets country `VN`                         | No `country-code` (null)                 | Excluded                    |
| Item targets version `1.2.0–2.0.0`                | `app-version: 1.5.0`                     | Included                    |
| Item targets version `1.2.0–2.0.0`                | `app-version: 0.9.0`                     | Excluded                    |
| Item targets version `1.2.0–2.0.0`                | No `app-version` (null)                  | Excluded                    |
| Item targets country `VN` + version `1.2.0–2.0.0` | `country-code: VN`, `app-version: 1.5.0` | Included (both match)       |
| Item targets country `VN` + version `1.2.0–2.0.0` | `country-code: VN`, `app-version: 0.9.0` | Excluded (version mismatch) |

### Summary

- **Default items** (no targeting) are **always returned**, regardless of headers.
- **Targeted items** are only returned when the client sends matching `country-code` and/or `app-version` headers.
- If you don't send `country-code`, it is `null` — all country-targeted items are excluded.
- If you don't send `app-version`, it is `null` — all version-targeted items are excluded.
- When both country and version targeting exist on an item, **both must match** (AND logic).
- Targeting applies to the **Get App**, **Get Categories**, **Get Category Tags**, **Get Tags**, **Get Usecases**, **Get Styles** endpoints. **Get Style Detail** (by documentId) does **not** apply targeting.

---

## Notes

- All responses are cached server-side (configurable TTL, default 10 minutes). Cache keys are partitioned by `bundleId`, `country-code`, and `app-version` to prevent cross-context pollution.
- **Localized content**: `title` and `subTitle` are resolved server-side to a single string based on the `country-code` header. Resolution order: country-specific match → global default (no country) → `null`. No arbitrary fallback to the first entry.
- `configuration` is opaque JSON — parse it as-is without schema validation.
- **`configuration` usage**: parse the `configuration` object and merge/inject it into the **body of the execute workflow** request when invoking the processing service. The CMS does not transform it; the client/service is responsible for applying it as workflow input parameters.
- StyleBlock fields (`documentId`, `code`, `serviceUrl`, etc.) can be `null` if the referenced style was unpublished or deleted.
- Media objects include both `url` (original full URL) and `path` (domain stripped). Use `path` when constructing custom CDN URLs.
- **Tags filter** uses OR logic: a style is included if it has at least one of the requested tags.