---
title: Custom Offer Unit (COU)
slug: custom-offer-unit-cou
icon: 🖍️
docTags: 
createdAt: 2026-06-25T21:27:08.172Z
---

:::hint{type="info"}
**Who is this for:** Partners who want Native API-level control over their Offer Unit's design, without building and maintaining a full Native API integration.

**Outcome:** By the end of this guide, you'll have a COU theme built, validated, previewed, saved, and live, with your HTML/CSS fully isolated from the host page and zero changes needed to your existing client-side integration.
:::

# Overview

MomentScience offers three ways to render Offer Units, and COU is the middle ground between them:

- **Standard SDK (SOU/MOU):** MomentScience's theme settings control the look and feel. Quick to configure, no code required, but layout is limited to what those settings expose.
- **Native API:** Full control over the UI. You build, host, and maintain the entire rendering layer yourself.
- **COU:** Native API-level design freedom, your own HTML and CSS, rendered through the SDK integration you already have. No separate build to maintain, and no client-side code changes.

In practice, you paste in your HTML and CSS, drop in a few placeholders for things like the offer title and image, and MomentScience takes care of the rest, filling in real offer data and hooking up buttons like "close" and "get offer" automatically. Your design is kept completely separate from your website's styling, so nothing clashes either way.

**Use COU when:**

- You need a fully branded or pixel-perfect ad layout
- Standard theme component settings (Header, Lead-in, Mini Text, TnC, etc.) don't give you enough control
- You're integrating offers into a UI that requires a specific visual structure your brand owns

:::hint{type="info"}
Contact your MomentScience account manager to request access to COU. Once enabled, you can see it as an option alongside SOU and MOU in the Offer Unit Type dropdown on the MomentPerks Integration page.
:::

# Setting Up a COU Theme

:::Iframe{iframeHeight="0" code="<div style=&#x22;position: relative; box-sizing: content-box; max-height: 80vh; max-height: 80svh; width: 100%; aspect-ratio: 2.11; padding: 40px 0 40px 0;&#x22;>&#xA;  <iframe src=&#x22;https://play.momentscience.com/embed/cmrl74roy1tgoqmvwx055h02i?embed_v=2&utm_source=embed&#x22; loading=&#x22;lazy&#x22; title=&#x22;Create and Deploy Custom Offer Units on MomentScience&#x22; allow=&#x22;clipboard-write&#x22; frameborder=&#x22;0&#x22; webkitallowfullscreen=&#x22;true&#x22; mozallowfullscreen=&#x22;true&#x22; allowfullscreen style=&#x22;position: absolute; top: 0; left: 0; width: 100%; height: 100%;&#x22;></iframe>&#xA;</div>"}

:::

## Step 1: Select COU as the Offer Unit Type

On your [MomentPerks Integrations page](https://app.momentscience.com/account/integration/customize#customize), open the **Offer Unit Type** dropdown and select **Custom Offer Unit**.

When COU is selected:

- Standard component tabs (Header, Lead-in, Mini Text, TnC, and others) are **hidden**; they don't apply when your HTML owns the full layout
- A **Custom Offer Unit** tab appears as your primary editing surface
- The **Theme Settings** tab remains visible

Switching back to SOU or MOU restores all standard component tabs

## Step 2: Build Your Template

The **Custom Offer Unit** tab contains two editors:

- **HTML Template editor**: Paste or type your HTML markup. Use macros (see below) as placeholders for dynamic offer content. The editor supports syntax highlighting and line numbers.
- **CSS Stylesheet editor**: Paste or type the CSS rules that style your HTML template.

:::hint{type="info"}
If you want to learn more about the available macros, expand the **Macro Reference&#x20;**&#x70;anel to view descriptions for each macro. Required macros are marked with an asterisk (\*).
:::

### 2.1: Use Macros for Dynamic Offer Content

Macros are placeholders in your HTML that MomentScience fills with real offer data at render time.

| **Macro**                             | **Description**                                                                           | **Required** |
| ------------------------------------- | ----------------------------------------------------------------------------------------- | ------------ |
| \{\{title}}                           | Offer headline. The one field worth always supplying.                                     | Yes          |
| \{\{description}}                     | Offer description. Rendered as raw HTML when used alone.                                  | No           |
| \{\{short\_headline\|title}}          | Short headline with fallback to title, then a quoted literal.                             | No           |
| \{\{short\_description\|description}} | Short description, falling back to full description, then a literal.                      | No           |
| \{\{advertiser\_name}}                | Brand name, fallback chain across campaign and offer.                                     | No           |
| \{\{cta\_yes}}                        | Primary CTA button copy.                                                                  | Yes          |
| \{\{cta\_no\|'No thanks'}}            | Secondary / decline button copy.                                                          | No           |
| \{\{terms\_and\_conditions}}          | T\&C URL. Used as a plain href (double braces are fine here).                             | No           |
| \{\{\{image}}}                        | Offer-level image, common fallback when hero\_image is absent. Use triple braces.         | No           |
| \{\{\{hero\_image}}}                  | Hero / background image URL (campaign\_images creative type). Use triple braces.          | No           |
| \{\{\{icon\_image}}}                  | Brand icon URL (campaign\_images creative type). Use triple braces.                       | No           |
| \{\{\{logo\_image}}}                  | Logo image URL (campaign\_images creative type). Use triple braces.                       | No           |
| \{\{lead\_in}}                        | Lead-in / eyebrow copy from styles.header.text. Use a quoted fallback.                    | No           |
| \{\{header\_text}}                    | Header / subhead copy from styles.header (lead\_in\_text / header\_text).                 | No           |
| \{\{#if key}} … \{\{/if}}             | Conditional block. Truthy unless undefined, null, or "". Supports \{\{else}} and nesting. | No           |

:::hint{type="warning"}
Macros used in `src`, `href`, or `background-image` attributes must use triple braces `{{{ }}}`. Double braces will URL-encode the value and break the link or image.
:::

:::hint{type="success"}
For more information about how macros work, see the [Implementing Macros](docId\:u1M7W0zxFxch7TzK6-gFJ)documentation.
:::

### 2.2: Wire Up Actions

Actions are data-ms-action attributes that wire elements in your HTML to MomentScience's built-in behavior, click tracking, carousel navigation, and closing. Add them as plain HTML attributes.

| **Action**                        | **Add to**                                                                                                          | **What it does**                                                                                                                                                                                                                                                                          | **Required** |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `data-ms-custom-root`             | Your outermost wrapper element                                                                                      | Marks the root of your template. Every other action depends on this being present.                                                                                                                                                                                                        | Yes          |
| `data-ms-action="cta"`            | Your primary offer element (e.g. the main CTA button)                                                               | Fires `ad_taken` and `url_clicked`, then opens `click_url` itself; no click-URL macro needed. Advances to the next offer, or closes the unit on the last.                                                                                                                                 | Yes          |
| `data-ms-action="close"`          | Your close control                                                                                                  | Closes the Offer Unit.                                                                                                                                                                                                                                                                    | Yes          |
| `data-ms-action="no-thanks"`      | Your decline / secondary button                                                                                     | Fires `ad_not_taken` and a `no_thanks` beacon. Advances to the next offer, or closes the unit on the last.                                                                                                                                                                                | No           |
| `data-ms-action="next"`           | A carousel "next" control                                                                                           | Advances to the next offer. Hidden on the last offer by default, or wraps to the first offer when `data-ms-loop="true"`. Hidden automatically when only one offer is returned.                                                                                                            | No           |
| `data-ms-action="prev"`           | A carousel "previous" control                                                                                       | Goes back to the previous offer. Hidden on the first offer by default, or wraps to the last offer when `data-ms-loop="true"`. Hidden automatically when only one offer is returned.                                                                                                       | No           |
| `data-ms-action="offerwall"`      | An "offerwall" CTA element                                                                                          | Opens the offerwall. Requires `enable_offerwall` and `offerwall_show_cta`; respects USP gating.                                                                                                                                                                                           | No           |
| `data-ms-action="save-for-later"` | A "Save for Later" element (e.g. a button)                                                                          | Saves the offer to the user's PerksWallet. Renders the configured PerksWallet CTA text by default, then switches to the Saved Offer Text once saved; won't re-trigger the save on further clicks.<br />* Requires PerksWallet to be enabled for the account and the offer to be eligible. | No           |
| `data-ms-loop="true"`             | Both your <br />`data-ms-action="next"`<br /> and <br />`data-ms-action="prev"`<br /> elements; add it to each one  | Keeps both arrows visible at every position and wraps next/prev between the first and last offer. Omit it, or set it to "false", to keep the default non-looping behavior.<br />* [Learn more about configuring loop navigation.](docId:1MCOuWTRS5te1lC6BehSo)                            | No           |

**Minimal Example Template**

:::CodeblockTabs
Example HTML Code

```html
<div class="offer-card" data-ms-custom-root>
  <button class="offer-card__close" data-ms-action="close">&times;</button>
  <img src="{{{offer_image}}}" alt="" />
  <h2>{{title}}</h2>
  <p>{{description}}</p>
  <a href="{{{click_url}}}" data-ms-action="cta">Get Offer</a>
</div>
```

Example CSS Code

```css
.offer-card {
	position: relative;
	background: #ffffff;
	border-radius: 12px;
	padding: 16px;
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
	font-family: sans-serif;
	max-width: 400px;
}

.offer-card__close {
	position: absolute;
	top: 8px;
	right: 8px;
	border: none;
	background: none;
	font-size: 18px;
	line-height: 1;
	cursor: pointer;
	color: #555555;
}

.offer-card img {
	width: 100%;
	border-radius: 8px;
	margin-bottom: 12px;
}

.offer-card h2 {
	margin: 0 0 8px;
	font-size: 18px;
	color: #111111;
}

.offer-card p {
	margin: 0 0 16px;
	font-size: 14px;
	color: #555555;
}

.offer-card a {
	display: inline-block;
	padding: 10px 20px;
	background: #0057ff;
	color: #ffffff;
	border-radius: 6px;
	text-decoration: none;
	font-weight: 600;
}
```
:::

### 2.3: Add a Save for Later Button

Add `data-ms-action="save-for-later"` to any element in your template, typically a `<button>`, to let users save the offer to their PerksWallet:

```html
<button type="button" data-ms-action="save-for-later"></button>
```

- No macro or inner text is required. MomentScience automatically fills the element with the account's configured **PerksWallet CTA** text, and swaps it to the **Saved Offer Text** once the offer is saved.
- Once saved, the button reflects the saved state and won't re-trigger the save action if clicked again.
- Requires **Show PerksWallet CTA** to be enabled for the account (see [Theme Customizer](#)) and the offer itself to be PerksWallet-eligible. If either isn't true, the element simply doesn't activate.
- Works the same way in both modal and embedded presentations, no extra configuration needed.

:::hint{type="warning"}
Enabling the PerksWallet setting alone does not add a button to a COU theme. Because COU renders your own HTML layout, you must add `data-ms-action="save-for-later"` to an element yourself for the button to appear.
:::

### 2.4: Configure Loop Navigation

By default, arrow visibility follows the offer's position in the sequence:

- **Single-offer sequence**: no arrows.
- **First offer** (2-5 offer sequence): back arrow hidden, next arrow shown.
- **Middle offer**: both arrows shown.
- **Last offer**: next arrow hidden, back arrow shown.

Add `data-ms-loop="true"` to your `data-ms-action="next"` and `data-ms-action="prev"` buttons to loop instead:

- Both arrows stay visible at every position.
- Next on the last offer wraps to the first.
- Back on the first offer wraps to the last.

:::hint{type="info"}
The setting applies per template instance, so different COU placements on the same page can be configured independently. It covers sequences of up to five offers, matching the existing sequence limit, and doesn't affect existing integrations unless added.
:::

### 2.5: Automatic Image Rotation for A/B Testing

When an offer has more than one Campaign Image uploaded, MomentScience automatically rotates across them, serving each to an equal share of sessions. This lets you run simple creative A/B tests just by uploading more than one image, there's nothing to configure or turn on.

Rotation applies wherever your template references a Campaign Image macro (`{{{image}}}`, `{{{hero_image}}}`, `{{{icon_image}}}`, `{{{logo_image}}}`), no changes to your HTML or CSS are needed.

## Step 3: Preview and Save

As you edit, the live preview panel re-renders your changes as you go, using the same preview offer and mobile/desktop toggle already on your Integrations page. If a required action is missing from your HTML, Save is blocked and an inline error names it below the HTML editor.

**When you're ready to save:**

- **Save**: Saves your changes to the current theme.
- **Save As**: Creates a new theme from your changes. You'll be prompted for a new theme ID, enter it and click **Confirm**.

:::hint{type="success"}
If you manage multiple themes, we recommend reading the [Managing Multiple Themes](docId:7_jtaiiskgwAu7cc5Nbp3)  documentation to learn best practices and how to work with them effectively.
:::

## Step 4: Go Live

No code changes are needed to render a COU theme. Once the theme is saved, the existing SDK snippet already on the page picks it up automatically: `launcher.js` resolves `theme_id` at render time and serves the saved HTML/CSS in place of the standard Offer Unit. Any change made in the dashboard, whether to HTML, CSS, or theme settings, takes effect immediately and is rendered the next time the Offer Unit loads.

:::hint{type="warning"}
Add the snippet below to the page, between the  tags, to show the Offer Unit. Make sure theme\_id matches the COU theme's ID.
:::

### Implementation

&#x20;Use this for a single placement on the page: one `accountId`, one call to `MS.init`. Set `theme_id` to the COU theme's ID if the account uses more than one theme; omit it to use the account's default theme.

:::CodeblockTabs
Implementation

```html
<script>
  // Queue stub (optional; harmless for sync loads, critical for async)
  window.MS = window.MS || function () { (MS.q = MS.q || []).push(arguments); };
</script>

<script src="https://sdk-js.pubtailer.com/launcher.latest.js"></script>

<script>
  var config = {
    accountId: 'AccountID', // Replace with a test or showcase account ID/SDK ID.
    // theme_id: '',   // optional - set if this account uses a custom theme
    user: {
       /**
       * Fill this object with user-specific values from your website. These
       * values are picked up by the SDK and are used to personalise the ads delivery and increase yield.
       */
      placement: '', // Replace with the placement name (e.g. 'right_rail')
    },
    userCallback: function (event, data) {
      // every SDK event lands here - see Callbacks below
    },
  };
  MS.init(config, 'instance_name'); // Replace 'instance_name' with a unique identifier for this placement.
</script>
```
:::

### Multi-Instance Implementation

Use this when the same page renders more than one placement. Each placement gets its own config object and its own `MS.init` call, since each can carry a different `accountId`, `theme_id`, or `user` block. The queue stub and `launcher.js` script tag are shared across all instances and load once per page.

:::CodeblockTabs
Multi-Instance Implementation

```html
<script>
  // Queue stub (optional; harmless for sync loads, critical for async)
  window.MS = window.MS || function () { (MS.q = MS.q || []).push(arguments); };
</script>

<script src="https://sdk-js.pubtailer.com/launcher.latest.js"></script>

<script>
  var configA = {
    accountId: 'AccountID', // Replace with a test or showcase account ID.
    // theme_id: '',   // optional - set if this account uses a custom theme
    user: {
      /**
       * Fill this object with user-specific values from your website. These
       * values are picked up by the SDK and are used to personalise the ads delivery and increase yield.
       */
      placement: '', // Replace with the placement name (e.g. 'Checkout')
    },
    userCallback: function (event, data) {
      // every SDK event lands here - see Callbacks below
    },
  };

  var configB = {
    accountId: 'AccountID', // Replace with a test or showcase account ID.
    // theme_id: '',   // optional - set if this account uses a custom theme
    user: {
       /**
       * Fill this object with user-specific values from your website. These
       * values are picked up by the SDK and are used to personalise the ads delivery and increase yield.
       */
      placement: '', // Replace with the placement name (e.g. 'OrderHistory')
    },
    userCallback: function (event, data) {
      // every SDK event lands here - see Callbacks below
    },
  };

  var instance1 = MS.init(configA, 'instance1'); // Rename to match the placement (e.g. 'homepage', 'checkout').
  var instance2 = MS.init(configB, 'instance2'); // Rename to match the placement.
</script>
```
:::

The instance names (`instance1`, `instance2`) are arbitrary; name each after its placement rather than its position.

***

If you're running into any issues while going through the integration process, feel free to contact us at [help@momentscience.com](mailto\:help@Momentscience.com)
