Custom Offer Unit (COU)
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
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
Step 1: Select COU as the Offer Unit Type
On your MomentPerks Integrations page, 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.
If you want to learn more about the available macros, expand the Macro Reference panel 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 |
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.
For more information about how macros work, see the Implementing Macros๏ปฟ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.
| No |
data-ms-loop="true" | ย Your carousel "next" and "prev" controls (elements with data-ms-action="next" / data-ms-action="prev") | Enables loop navigation: both arrows stay visible at every position, and next/prev wrap between the first and last offer. Omit, or set to "false", for the default non-looping behavior. | No |
Minimal Example Template
<div class="offer-card" data-ms-custom-root>
<button class="offer-card__close" data-ms-action="close">×</button>
<img src="{{{offer_image}}}" alt="" />
<h2>{{title}}</h2>
<p>{{description}}</p>
<a href="{{{click_url}}}" data-ms-action="cta">Get Offer</a>
</div>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:
<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.
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.
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.
If you manage multiple themes, we recommend reading the Managing Multiple Themes๏ปฟ 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.
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
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.
<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.
<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๏ปฟ