# Native in-app templates syntax

Native in-apps are rendered directly by the SDK. No WebView is involved. Instead of an `index.html` page, the ZIP resource carries a `native-config.json` file that describes the message as structured data (layout type, texts, colors, images, buttons). The SDK reads this file and draws the matching native view, which gives smoother animations and better performance than an embedded web page.

This guide documents the `native-config.json` schema: fields, types, and examples for each display type. For the classic HTML-based format, see [Rich media templates syntax](/developer/guides/content/rich-media-templates-syntax/).

## Prerequisites

Native in-apps require:

* **iOS:** SDK 7.2.0 or later (7.2.1+ for banner, carousel, and sheet)
* **Android:** SDK 6.10.0 or later (6.10.1+ for banner, carousel, and sheet)

Not every display type is available on both platforms yet. Check [Platform support](#platform-support) before you rely on a specific format.

## Platform support

| Display type <div style="width:130px"></div> | iOS <div style="width:130px"></div>        | Android <div style="width:170px"></div>   |
| -------------------------------------------- | ------------------------------------------ | ----------------------------------------- |
| modal                                        | ✅ 7.2.0+                                  | ✅ 6.10.0+                                |
| fullscreen                                   | ✅ 7.2.0+                                  | ✅ 6.10.0+                                |
| stories                                      | ✅ 7.2.0+                                  | ✅ 6.10.0+                                |
| banner                                       | ✅ 7.2.1+                                  | ✅ 6.10.1+                                |
| carousel                                     | ✅ 7.2.1+                                  | ✅ 6.10.1+                                |
| sheet                                        | ✅ 7.2.1+                                  | ✅ 6.10.1+                                |
| video                                        | ✅                                         | Not yet available                         |
| pip                                          | ✅                                         | Not yet available                         |
| scratchcard                                  | ✅                                         | Not yet available                         |
| spinwheel                                    | ✅                                         | Not yet available                         |

<Aside type="caution">
A `native-config.json` with a `displayType` your app's current SDK version doesn't implement is treated the same as an invalid config. The SDK skips it rather than showing a broken layout. Target campaigns to the right app version if you rely on a type that isn't available everywhere yet.
</Aside>

## Template structure

A native in-app template is a ZIP archive, same as a regular Rich Media template, except the root contains a **native-config.json** file instead of **index.html**:

```
<template>.zip
├── native-config.json   ← required, layout and content
├── pushwoosh.json        ← optional, localization (see below)
```

Images and videos referenced from `native-config.json` (`image`, `poster`, `fallback`, `url` on `pip`/`video`) must be absolute HTTPS URLs. The SDK loads them over the network. It does not read local files from the archive.

The config itself is a single JSON object:

```json
{ "displayType": "<type>", "<type>": { /* content block for this type */ } }
```

`displayType` selects one of the ten formats below. The object under the matching key holds that format's content. A config with an unknown `displayType`, a missing content block, or an empty required list (`items` for `carousel`/`stories`, `segments` for `spinwheel`) is invalid. The SDK skips showing it rather than rendering a broken layout.

Delivery settings (start/end dates and frequency capping) are **not** part of `native-config.json`. They're configured the same way as for any other in-app, in the campaign's [Display settings step](/product/messaging-channels/in-apps/send-in-apps/send-instant-in-apps/#configure-frequency-capping). 

Frequency capping additionally needs an explicit SDK-side opt-in to take effect on native in-apps. See [SDK integration](#sdk-integration).

## Colors

Every color value is a CSS hex string: `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA`. The leading `#` is required in all four forms.

## Shared building blocks

These smaller objects are reused across several display types.

### Text

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `text` | string | yes | The text content |
| `color` | string | yes | Text color |

```json
{ "text": "Spin for a garage perk", "color": "#FFFFFFFF" }
```

### Border

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `color` | string | yes | Border color |
| `radius` | number | yes | Corner radius, in points |

```json
{ "color": "#0E72E5FF", "radius": 12 }
```

### Cover

An optional image over a background color. Used by `fullscreen` and `scratchcard`.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `image` | string | no | Cover image URL |
| `background` | string | yes | Background color shown under (or instead of) the image |

```json
{ "image": "https://example.com/cover.jpg", "background": "#1A1A1EFF" }
```

### Action

A discriminated union on `type`:

| Variant | Fields | Description |
| --- | --- | --- |
| `{ "type": "close" }` | None | Closes the in-app |
| `{ "type": "url", "url": string }` | `url` required | Opens a URL or deep link |

```json
{ "type": "url", "url": "pushwoosh://sale" }
```

### Button

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `text` | Text | yes | Button label |
| `background` | string | yes | Button fill color |
| `border` | Border | yes | Button border |
| `action` | Action | yes | Action fired on tap |

`spinButton` (spinwheel) and `revealButton` (scratchcard) use the same shape **without** `action`. Their behavior (spin the wheel, reveal the card) is built in.

```json
{
  "text": { "text": "Book a test drive", "color": "#FFFFFFFF" },
  "background": "#0E72E5FF",
  "border": { "color": "#0E72E5FF", "radius": 12 },
  "action": { "type": "url", "url": "pushwoosh://sale" }
}
```

### Reward

The prize panel shown by `scratchcard` and `spinwheel`. A valid reward has a `title` or a `code`.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `title` | Text | no | Reward headline |
| `message` | Text | no | Reward description |
| `code` | string | no | Promo code, rendered with a copy button |
| `button` | Button | no | Confirmation button with its own action |

```json
{
  "title": { "text": "20% off detailing", "color": "#111111FF" },
  "message": { "text": "Valid for any full-detail booking this month.", "color": "#555555FF" },
  "code": "APEX20",
  "button": {
    "text": { "text": "Book detailing", "color": "#FFFFFFFF" },
    "background": "#B3227CFF",
    "border": { "color": "#B3227CFF", "radius": 12 },
    "action": { "type": "url", "url": "pushwoosh://detailing" }
  }
}
```

## Display types

### banner

A compact bar docked to the top or bottom edge of the screen.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `position` | `top` \| `bottom` | yes | Screen edge |
| `background` | string | yes | Bar background color |
| `image` | string | no | Thumbnail on the left |
| `title` | Text | no | Single-line title, truncated with an ellipsis |
| `message` | Text | no | Body text, up to 2 lines |
| `action` | Action | yes | Fired when the bar itself is tapped |
| `autoDismiss` | number | no | Auto-close after this many seconds. Omit to keep it until closed |

```json
{
  "displayType": "banner",
  "banner": {
    "showClose": true,
    "position": "bottom",
    "background": "#4B5057FF",
    "image": "https://example.com/thumb.jpg",
    "title": { "text": "Alpine A110 just dropped", "color": "#FFFFFFFF" },
    "message": { "text": "The featherweight icon — tap to see the build", "color": "#FFFFFFFF" },
    "action": { "type": "url", "url": "pushwoosh://product/x6f" },
    "autoDismiss": 6
  }
}
```

### carousel

A full-screen, swipeable set of cards with page-indicator dots.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `items` | Item[] | yes | Cards (at least 1) |

Carousel item:

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `title` | Text | no | Card title |
| `message` | Text | no | Card subtitle |
| `image` | string | no | Card image |
| `action` | Action | no | Fired when the card is tapped |

```json
{
  "displayType": "carousel",
  "carousel": {
    "showClose": true,
    "items": [
      {
        "image": "https://example.com/card-1.jpg",
        "title": { "text": "AMG GT R", "color": "#FFFFFFFF" },
        "message": { "text": "585 hp biturbo V8 — just landed", "color": "#FFFFFFFF" },
        "action": { "type": "url", "url": "pushwoosh://product/n6fx" }
      },
      {
        "image": "https://example.com/card-2.jpg",
        "title": { "text": "Alpine A110", "color": "#FFFFFFFF" },
        "message": { "text": "Featherweight icon — limited allocation", "color": "#FFFFFFFF" },
        "action": { "type": "url", "url": "pushwoosh://product/x6f" }
      }
    ]
  }
}
```

### fullscreen

An edge-to-edge cover image with text and buttons on top.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `cover` | Cover | yes | Background image and color |
| `title` | Text | no | Title |
| `message` | Text | no | Body text |
| `buttons` | Button[] | yes | Buttons at the bottom (can be empty) |

```json
{
  "displayType": "fullscreen",
  "fullscreen": {
    "showClose": true,
    "cover": { "image": "https://example.com/hero.jpg", "background": "#1A1A1EFF" },
    "title": { "text": "Pure Maranello", "color": "#FFFFFFFF" },
    "message": { "text": "The prancing horse, reimagined.", "color": "#EBEBEBFF" },
    "buttons": [
      {
        "text": { "text": "Reserve now", "color": "#FFFFFFFF" },
        "background": "#0E72E5FF",
        "border": { "color": "#0E72E5FF", "radius": 8 },
        "action": { "type": "url", "url": "pushwoosh://sale" }
      },
      {
        "text": { "text": "Not now", "color": "#FFFFFFFF" },
        "background": "#00000000",
        "border": { "color": "#FFFFFF99", "radius": 8 },
        "action": { "type": "close" }
      }
    ]
  }
}
```

### modal

A centered card.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `dimBackground` | boolean | yes | Dim the screen behind the card |
| `background` | string | yes | Card background color |
| `image` | string | no | Cover image |
| `title` | Text | no | Title |
| `message` | Text | no | Body text |
| `buttons` | Button[] | yes | Buttons below the text (can be empty) |

```json
{
  "displayType": "modal",
  "modal": {
    "showClose": true,
    "dimBackground": true,
    "background": "#FFFFFFFF",
    "image": "https://example.com/cover.jpg",
    "title": { "text": "The GT R has landed", "color": "#4B5057FF" },
    "message": { "text": "585 hp — now in the showroom.", "color": "#4B5057FF" },
    "buttons": [
      {
        "text": { "text": "Book a test drive", "color": "#FFFFFFFF" },
        "background": "#0E72E5FF",
        "border": { "color": "#0E72E5FF", "radius": 12 },
        "action": { "type": "url", "url": "pushwoosh://sale" }
      },
      {
        "text": { "text": "Not now", "color": "#4B5057FF" },
        "background": "#FFFFFFFF",
        "border": { "color": "#4B5057FF", "radius": 12 },
        "action": { "type": "close" }
      }
    ]
  }
}
```

### pip

A floating picture-in-picture video window docked to a screen corner.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `position` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` | yes | Screen corner |
| `loop` | boolean | yes | Loop playback |
| `muted` | boolean | yes | Start muted |
| `url` | string | yes | Video URL |
| `poster` | string | no | Poster shown before playback starts |
| `fallback` | string | no | Image shown if the video fails to play |
| `width` | number | yes | Window width as a percentage of screen width, clamped 15–70 |
| `aspectRatio` | number | yes | Window height-to-width ratio |
| `borderRadius` | number | no | Window corner radius, in points |
| `action` | Action | no | Fired when the window itself is tapped |

There are no configurable buttons on `pip`. Window controls (expand to full screen, mute, close) are system-provided.

```json
{
  "displayType": "pip",
  "pip": {
    "showClose": true,
    "position": "bottom-right",
    "loop": true,
    "muted": true,
    "url": "https://example.com/teaser.mp4",
    "poster": "https://example.com/poster.jpg",
    "width": 40,
    "aspectRatio": 0.5625,
    "action": { "type": "url", "url": "pushwoosh://product/x6f" }
  }
}
```

### scratchcard

A card with the reward hidden under a scratchable foil layer.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `background` | string \| string[] | yes | Card background color, or gradient stops |
| `revealThreshold` | number | yes | Fraction of the foil that must be scratched off (0–1) before the reward reveals |
| `cover` | Cover | yes | The foil layer. Without an `image`, a "scratch here" hint is shown on the background color |
| `revealButton` | Button (no `action`) | no | "Reveal instantly" button |
| `title` | Text | no | Title |
| `message` | Text | no | Body text |
| `reward` | Reward | yes | The prize hidden under the foil |

```json
{
  "displayType": "scratchcard",
  "scratchcard": {
    "showClose": true,
    "background": ["#3A1C71FF", "#B3227CFF", "#E0503AFF"],
    "revealThreshold": 0.55,
    "cover": { "background": "#C9CDD6FF" },
    "revealButton": {
      "text": { "text": "Reveal without scratching", "color": "#3A1C71FF" },
      "background": "#F2DFF5FF",
      "border": { "color": "#F2DFF5FF", "radius": 10 }
    },
    "title": { "text": "Your loyalty reward", "color": "#FFFFFFFF" },
    "message": { "text": "Scratch the foil to reveal this week's garage perk.", "color": "#F2DFF5FF" },
    "reward": {
      "title": { "text": "20% off detailing", "color": "#111111FF" },
      "message": { "text": "Valid for any full-detail booking this month.", "color": "#555555FF" },
      "code": "APEX20",
      "button": {
        "text": { "text": "Book detailing", "color": "#FFFFFFFF" },
        "background": "#B3227CFF",
        "border": { "color": "#B3227CFF", "radius": 12 },
        "action": { "type": "url", "url": "pushwoosh://detailing" }
      }
    }
  }
}
```

### sheet

A card pinned to the bottom edge, with a drag handle.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `dimBackground` | boolean | yes | Dim the screen behind the sheet |
| `background` | string | yes | Sheet background color |
| `image` | string | no | Cover image |
| `title` | Text | no | Title |
| `message` | Text | no | Body text |
| `buttons` | Button[] | yes | Buttons below the text (can be empty) |

```json
{
  "displayType": "sheet",
  "sheet": {
    "showClose": true,
    "dimBackground": true,
    "background": "#FFFFFFFF",
    "image": "https://example.com/cover.jpg",
    "title": { "text": "Your quote is ready", "color": "#000000FF" },
    "message": { "text": "Guaranteed buyout for your A110: $68,500.", "color": "#000000FF" },
    "buttons": [
      {
        "text": { "text": "Get guaranteed quote", "color": "#FFFFFFFF" },
        "background": "#0E72E5FF",
        "border": { "color": "#0E72E5FF", "radius": 12 },
        "action": { "type": "url", "url": "pushwoosh://sale" }
      }
    ]
  }
}
```

### spinwheel

A wheel-of-fortune with weighted segments and a center hub button.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `background` | string \| string[] | yes | Card background color, or gradient stops |
| `winIndex` | number | yes | Index (0-based) of the winning segment |
| `spinButton` | Button (no `action`) | yes | Center hub button |
| `title` | Text | no | Title |
| `message` | Text | no | Body text |
| `reward` | Reward | yes | Reward for the winning spin (fallback for segments without their own) |
| `loseTitle` | Text | no | Headline shown on a loss |
| `segments` | Segment[] | yes | Wheel segments (SDK expects 2–12) |

Segment:

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `message` | Text | yes | Segment label |
| `color` | string | no | Segment color. Omit for a fallback palette applied around the wheel |
| `weight` | number | yes | Relative segment size |
| `reward` | Reward | no | Segment-specific reward |

```json
{
  "displayType": "spinwheel",
  "spinwheel": {
    "showClose": true,
    "background": ["#1B1B46FF", "#5B2B8FFF", "#B0338AFF"],
    "winIndex": 1,
    "spinButton": {
      "text": { "text": "SPIN", "color": "#1B1B46FF" },
      "background": "#F2C94CFF",
      "border": { "color": "#D9A02BFF", "radius": 36 }
    },
    "title": { "text": "Spin for a garage perk", "color": "#FFFFFFFF" },
    "message": { "text": "One spin — every slice wins this week.", "color": "#E3D9F2FF" },
    "reward": {
      "title": { "text": "You won a garage perk!", "color": "#FFFFFFFF" },
      "code": "APEXPERK",
      "button": {
        "text": { "text": "Claim", "color": "#FFFFFFFF" },
        "background": "#5B2B8FFF",
        "border": { "color": "#5B2B8FFF", "radius": 12 },
        "action": { "type": "close" }
      }
    },
    "segments": [
      { "message": { "text": "5% off", "color": "#FFFFFFFF" }, "color": "#5856D6FF", "weight": 1 },
      {
        "message": { "text": "20% off", "color": "#FFFFFFFF" },
        "color": "#30B0C7FF",
        "weight": 1,
        "reward": {
          "title": { "text": "20% off your next service", "color": "#FFFFFFFF" },
          "code": "SPIN20",
          "button": {
            "text": { "text": "Claim service deal", "color": "#FFFFFFFF" },
            "background": "#30B0C7FF",
            "border": { "color": "#30B0C7FF", "radius": 12 },
            "action": { "type": "url", "url": "pushwoosh://service" }
          }
        }
      },
      { "message": { "text": "Free wash", "color": "#FFFFFFFF" }, "color": "#FF2D55FF", "weight": 1 }
    ]
  }
}
```

### stories

Full-screen slides with progress bars at the top, similar to social media stories.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `loop` | boolean | yes | Restart from the first slide after the last one |
| `items` | Item[] | yes | Slides (at least 1) |

Stories item:

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `title` | Text | no | Title |
| `message` | Text | no | Subtitle |
| `image` | string | no | Slide background image |
| `buttons` | Button[] | yes | CTA buttons at the bottom (can be empty) |
| `duration` | number | yes | Slide duration, in seconds |

```json
{
  "displayType": "stories",
  "stories": {
    "showClose": true,
    "loop": false,
    "items": [
      {
        "image": "https://example.com/slide-1.jpg",
        "title": { "text": "AMG GT R", "color": "#FFFFFFFF" },
        "message": { "text": "The Green Hell special", "color": "#FFFFFFFF" },
        "buttons": [
          {
            "text": { "text": "Configure yours", "color": "#FFFFFFFF" },
            "background": "#0F0F0FFF",
            "border": { "color": "#0F0F0FFF", "radius": 26 },
            "action": { "type": "url", "url": "pushwoosh://product/n6fx" }
          }
        ],
        "duration": 4
      },
      {
        "image": "https://example.com/slide-2.jpg",
        "title": { "text": "Alpine A110", "color": "#FFFFFFFF" },
        "message": { "text": "The featherweight legend, reborn", "color": "#FFFFFFFF" },
        "buttons": [],
        "duration": 4
      }
    ]
  }
}
```

### video

Full-screen video with text and buttons on top.

| Field | Type | Required | Description |
| --- | --- | :---: | --- |
| `showClose` | boolean | yes | Show a close (✕) button |
| `loop` | boolean | yes | Loop playback |
| `muted` | boolean | yes | Start muted |
| `url` | string | yes | Video URL (HLS or MP4) |
| `poster` | string | no | Poster shown before playback starts |
| `fallback` | string | no | Image shown if the video fails to play |
| `title` | Text | no | Title |
| `message` | Text | no | Body text |
| `buttons` | Button[] | yes | CTA buttons at the bottom (can be empty) |

```json
{
  "displayType": "video",
  "video": {
    "showClose": true,
    "loop": true,
    "muted": true,
    "url": "https://example.com/reveal.mp4",
    "poster": "https://example.com/poster.jpg",
    "title": { "text": "The reveal", "color": "#FFFFFFFF" },
    "message": { "text": "Watch it move before anyone else.", "color": "#EBEBEBFF" },
    "buttons": [
      {
        "text": { "text": "Shop the lineup", "color": "#FFFFFFFF" },
        "background": "#0E72E5FF",
        "border": { "color": "#0E72E5FF", "radius": 14 },
        "action": { "type": "url", "url": "pushwoosh://sale" }
      }
    ]
  }
}
```

## Localization

Native in-apps reuse the exact same localization mechanism as HTML Rich Media: string values in `native-config.json` can carry `{{key|type|default}}` placeholders, and translations live in a **pushwoosh.json** file next to it, in the same format described in [Adding pushwoosh.json](/developer/guides/content/rich-media-templates-syntax/#adding-pushwooshjson). A placeholder can appear in any string field, at any depth (a title, a button label, an image URL, an action URL).

<Aside type="note">
Dynamic content (personalizing native in-app text with the recipient's own tags) isn't available yet. It's planned for a future release. Today, an unresolved placeholder falls back to its default value.
</Aside>

## SDK integration

Once you add the native in-app SDK module to your app, messages display automatically. No extra code is required to show messages triggered by a push, Customer Journey, `postEvent`, or the inbox.

The SDK also exposes a small API for manual control:

* **iOS:** `Pushwoosh.inApp` (module `PushwooshInApp`)
* **Android:** `PushwooshInAppUi` (module `pushwoosh-inapp-ui`)

| Capability | iOS | Android |
| --- | --- | --- |
| Show a config directly (testing/manual use) | `Pushwoosh.inApp.present(config)` | `PushwooshInAppUi.present(configJson)` |
| Observe lifecycle and clicks | `delegate` (`PWInAppMessageDelegate`) | `delegate` (`InAppMessageDelegate`) |
| Check if something is on screen | `isPresenting` | `isPresenting` |
| Dismiss whatever is currently shown | `dismiss()` | `dismiss()` |
| Pause / resume display | `isPaused` | `isPaused` |
| Enforce `maxDisplays` / `cooldown` capping | `setFrequencyCapEnabled(_:)` | `setFrequencyCapEnabled(...)` |

<Aside type="caution">
Frequency capping configured in the campaign's [Display settings](/product/messaging-channels/in-apps/send-in-apps/send-instant-in-apps/#configure-frequency-capping) is **not enforced automatically** for native in-apps, unlike classic Rich Media. The SDK ignores `maxDisplays`/`cooldown` until you explicitly call `setFrequencyCapEnabled(true)`. The message's start/end dates are always enforced regardless of this setting.
</Aside>

Delegate callbacks (all fired on the main thread): `shouldDisplay` (return `false` to suppress one message before it shows, for example on a checkout screen), `willPresent`, `didPresent`, `didClose`, and `clickedAction` (fired when the user taps a `url` action, before the URL opens).

iOS additionally reports `rewardRevealed` and `rewardClaimed` for the gamified `scratchcard` and `spinwheel` templates.

```swift
// iOS
Pushwoosh.inApp.delegate = self
Pushwoosh.inApp.setFrequencyCapEnabled(true)
```

```kotlin
// Android
PushwooshInAppUi.delegate = this
PushwooshInAppUi.setFrequencyCapEnabled(true)
```