HTML in-app messages
An HTML message talks to your app or site through one object, treebars. Its methods are the standard ones, and most messages need none of them, because a few attributes do the tracking.
Where it runs
The script in a message — yours, and the small runtime we add for the attributes below — runs where the SDK hosts the bridge: the Web SDK 0.4.0 and the Android SDK 0.4.0 and the iOS SDK 0.4.0 and later. The Android and iOS SDKs host it from their next release; until then an app draws the markup with scripts off, so keep a way out that needs none, such as a link to treebars://dismiss. The editor says how many of your devices can run it.
The message is drawn in a sandboxed frame: it cannot read your page or your app's storage, and it has no fetch, XHR or WebSocket. Images, fonts and styles load from any https address. Every link goes to the SDK, which records the press and then opens it.
frame-src https://frame.treebars.com to your policy. A site that allows no outside frame shows no HTML message, and the campaign report counts it as blocked.On a website the SDK shows a message with no set-up past init: In-app messages on a website has when it appears, how to switch it off, and what to check when it does not.
Tracking without writing any
These work with no script of your own. Put them on the elements, and the message does the rest.
| Attribute | Example | What it does |
|---|---|---|
data-tb-screen | data-tb-screen="welcome" | One screen of a flow. The first shows; the rest wait. data-tb-final on a screen records the flow as completed. |
data-tb-id | data-tb-id="cta" | Reports the element’s clicks under that id. |
data-tb-action | next, back, screen:done, dismiss, submit, deeplink:app://orders, url:https://…, landing:https://…, push, copy:CODE, share:text, store-review, custom, app:add_to_cart, navigate:Cart | Acts, after the click is recorded. app: and navigate: hand your app a named action, with data-tb-props as its payload, which it subscribes to with onAction (SDK 0.11.0). |
data-tb-event | data-tb-event="offer_claimed" data-tb-props='{"plan":"annual"}' | Records one of your events. |
data-tb-save | data-tb-save="email" | Keeps the answer when the form submits: an email or phone as an opt-in, anything else as a trait. |
data-tb-then | data-tb-then="next" | On whatever submits — the button, or its form — or on a push ask: where the message goes once the answers are sent or the permission is answered, such as a thanks screen or dismiss. |
data-tb-track | data-tb-track="off" | Turns automatic click tracking off inside it. |
- Attribute
data-tb-screen- Example
- data-tb-screen="welcome"
- What it does
- One screen of a flow. The first shows; the rest wait. data-tb-final on a screen records the flow as completed.
- Attribute
data-tb-id- Example
- data-tb-id="cta"
- What it does
- Reports the element’s clicks under that id.
- Attribute
data-tb-action- Example
- next, back, screen:done, dismiss, submit, deeplink:app://orders, url:https://…, landing:https://…, push, copy:CODE, share:text, store-review, custom, app:add_to_cart, navigate:Cart
- What it does
- Acts, after the click is recorded. app: and navigate: hand your app a named action, with data-tb-props as its payload, which it subscribes to with onAction (SDK 0.11.0).
- Attribute
data-tb-event- Example
- data-tb-event="offer_claimed" data-tb-props='{"plan":"annual"}'
- What it does
- Records one of your events.
- Attribute
data-tb-save- Example
- data-tb-save="email"
- What it does
- Keeps the answer when the form submits: an email or phone as an opt-in, anything else as a trait.
- Attribute
data-tb-then- Example
- data-tb-then="next"
- What it does
- On whatever submits — the button, or its form — or on a push ask: where the message goes once the answers are sent or the permission is answered, such as a thanks screen or dismiss.
- Attribute
data-tb-track- Example
- data-tb-track="off"
- What it does
- Turns automatic click tracking off inside it.
<main>
<section data-tb-screen="welcome">
<h1>Welcome, {{ user.first_name | default: "there" }}</h1>
<button data-tb-id="start" data-tb-action="next">Get started</button>
<button data-tb-id="skip" data-tb-action="dismiss">Not now</button>
</section>
<section data-tb-screen="interests" hidden>
<fieldset data-tb-save="interests">
<label><input type="checkbox" value="shoes"> Shoes</label>
<label><input type="checkbox" value="kitchen"> Kitchen</label>
</fieldset>
<button data-tb-id="continue" data-tb-event="interests_selected" data-tb-action="next">Continue</button>
</section>
<section data-tb-screen="done" data-tb-final hidden>
<p>Your code is SPRING10.</p>
<button data-tb-id="copy" data-tb-action="copy:SPRING10">Copy code</button>
<a href="treebars://dismiss">Close</a>
</section>
</main>The methods
Every method returns a Promise, and every one resolves: a refusal is { ok: false, reason }, never an error, so a template brought over from another platform — written against a bridge that returns nothing, and never catching — behaves as it did there.
Outcomes
Recording what the person did with the message, and closing it.
| Method | What it does | Where |
|---|---|---|
dismissMessage() | Records in_app_dismissed (once per display) and closes the message. Closing this way IS counted, so a template need not record the dismissal itself first. | All |
trackDismiss(widgetId?) | Records in_app_dismissed with the element; does not close. A dismissMessage() after it does not count twice. | All |
trackClick(widgetId?) | Records in_app_clicked with the element (a whole number is also button_index); neither closes nor navigates. A widgetId of 0 is kept as "0" rather than read as no id at all. | All |
trackRating(rating) | Records in_app_rated with the rating. | All |
- Method
dismissMessage()- What it does
- Records
in_app_dismissed(once per display) and closes the message. Closing this way IS counted, so a template need not record the dismissal itself first. - Where
- All
- Method
trackDismiss(widgetId?)- What it does
- Records
in_app_dismissedwith the element; does not close. AdismissMessage()after it does not count twice. - Where
- All
- Method
trackClick(widgetId?)- What it does
- Records
in_app_clickedwith the element (a whole number is alsobutton_index); neither closes nor navigates. AwidgetIdof0is kept as"0"rather than read as no id at all. - Where
- All
- Method
trackRating(rating)- What it does
- Records
in_app_ratedwith the rating. - Where
- All
Events
Your own events, attributed to the campaign every time.
| Method | What it does | Where |
|---|---|---|
trackEvent(eventName, generalAttrJson?, locationAttrJson?, dateAttrJson?, isNonInteractive?, shouldAttachCampaignMeta?) | Records the event with the campaign, delivery and message always attached; the location and date objects are merged into its properties. At most 20 a display, and a repeat of one name within 250 ms is dropped. A name starting in_app_ or reserved by the SDK is refused. The fifth and sixth arguments are accepted and IGNORED: a message on screen is activity, and the campaign is always attached. | All |
- Method
trackEvent(eventName, generalAttrJson?, locationAttrJson?, dateAttrJson?, isNonInteractive?, shouldAttachCampaignMeta?)- What it does
- Records the event with the campaign, delivery and message always attached; the location and date objects are merged into its properties. At most 20 a display, and a repeat of one name within 250 ms is dropped. A name starting
in_app_or reserved by the SDK is refused. The fifth and sixth arguments are accepted and IGNORED: a message on screen is activity, and the campaign is always attached. - Where
- All
Identity
Present by name, and refused: a message cannot change who is reading it.
| Method | What it does | Where |
|---|---|---|
identifyUser(idOrAttributes) | Refuses, and lint flags it at publish. Who a device belongs to is decided by the app, never by a message. | All |
setUniqueId(uniqueId) | Refuses, and lint flags it at publish. Who a device belongs to is decided by the app, never by a message. | All |
setAlias(alias) | Refuses, and lint flags it at publish. Who a device belongs to is decided by the app, never by a message. | All |
- Method
identifyUser(idOrAttributes)- What it does
- Refuses, and lint flags it at publish. Who a device belongs to is decided by the app, never by a message.
- Where
- All
- Method
setUniqueId(uniqueId)- What it does
- Refuses, and lint flags it at publish. Who a device belongs to is decided by the app, never by a message.
- Where
- All
- Method
setAlias(alias)- What it does
- Refuses, and lint flags it at publish. Who a device belongs to is decided by the app, never by a message.
- Where
- All
Profile
Addresses as opt-ins, and traits on a signed-in person.
| Method | What it does | Where |
|---|---|---|
setEmailId(email) | Keeps the address as this person’s opt-in (the in-app form’s save_as path), signed in or not; never changes who they are. | All |
setMobileNumber(mobileNumber) | Keeps the number as this person’s opt-in, as setEmailId does. | All |
setUserName(value) | Sets the trait name. | All |
setFirstName(value) | Sets the trait first_name. | All |
setLastName(value) | Sets the trait last_name. | All |
setGender(value) | Sets the trait gender. | All |
setBirthDate(value) | Sets the trait birth_date (an ISO 8601 date). | All |
setUserLocation(latitude, longitude) | Sets the trait location. | All |
setUserAttribute(name, value) | Sets a trait the template declares in its manifest; a platform trait or an undeclared one is refused. | All |
setUserAttributeDate(name, iso8601) | As setUserAttribute, for a date. | All |
setUserAttributeLocation(name, latitude, longitude) | As setUserAttribute, for a location. | All |
- Method
setEmailId(email)- What it does
- Keeps the address as this person’s opt-in (the in-app form’s
save_aspath), signed in or not; never changes who they are. - Where
- All
- Method
setMobileNumber(mobileNumber)- What it does
- Keeps the number as this person’s opt-in, as
setEmailIddoes. - Where
- All
- Method
setUserName(value)- What it does
- Sets the trait
name. - Where
- All
- Method
setFirstName(value)- What it does
- Sets the trait
first_name. - Where
- All
- Method
setLastName(value)- What it does
- Sets the trait
last_name. - Where
- All
- Method
setGender(value)- What it does
- Sets the trait
gender. - Where
- All
- Method
setBirthDate(value)- What it does
- Sets the trait
birth_date(an ISO 8601 date). - Where
- All
- Method
setUserLocation(latitude, longitude)- What it does
- Sets the trait
location. - Where
- All
- Method
setUserAttribute(name, value)- What it does
- Sets a trait the template declares in its manifest; a platform trait or an undeclared one is refused.
- Where
- All
- Method
setUserAttributeDate(name, iso8601)- What it does
- As
setUserAttribute, for a date. - Where
- All
- Method
setUserAttributeLocation(name, latitude, longitude)- What it does
- As
setUserAttribute, for a location. - Where
- All
Navigation
Each records the press before it goes anywhere. None closes the message.
| Method | What it does | Where |
|---|---|---|
navigateToScreen(screenName, dataJson?) | Records the click, then hands the app a screen and its key-values (setClickActionListener, the iOS delegate, inAppCampaignCustomAction; on the web, the TREEBARS_ONSITE_MESSAGE_CLICKED event). | All |
openDeepLink(url, dataJson?) | Records the click, then opens the deep link. | All |
openRichLanding(url, dataJson?) | Records the click, then opens the page in an in-app browser (a new tab on the web). | All |
openWebURL(url, dataJson?) | Records the click, then opens the page in the system browser (this tab on the web). | All |
- Method
navigateToScreen(screenName, dataJson?)- What it does
- Records the click, then hands the app a screen and its key-values (
setClickActionListener, the iOS delegate,inAppCampaignCustomAction; on the web, theTREEBARS_ONSITE_MESSAGE_CLICKEDevent). - Where
- All
- Method
openDeepLink(url, dataJson?)- What it does
- Records the click, then opens the deep link.
- Where
- All
- Method
openRichLanding(url, dataJson?)- What it does
- Records the click, then opens the page in an in-app browser (a new tab on the web).
- Where
- All
- Method
openWebURL(url, dataJson?)- What it does
- Records the click, then opens the page in the system browser (this tab on the web).
- Where
- All
Device
What the host app or the browser can do for the page.
| Method | What it does | Where |
|---|---|---|
copyText(text, message?) | Copies the text, and shows message as a toast. navigator.clipboard.writeText inside a message is routed here, because a sandbox or a WebView usually refuses it. | All |
call(mobileNumber) | Opens the dialer with the number. | All |
sms(mobileNumber, message?) | Opens the SMS composer with the number and the text. | All |
share(text) | Opens the share sheet (on the web, where the browser has one). | All |
customAction(dataJson) | Records the click, then hands the key-values to the app: setClickActionListener on Android, the delegate’s inAppClicked on iOS, inAppCampaignCustomAction in React Native, TREEBARS_ONSITE_MESSAGE_CLICKED on the web. An object is accepted as well as a JSON string. | All |
requestNotificationPermission() | Asks for push permission, and resolves with the answer. | All |
showPushOptIn() | The same as requestNotificationPermission, under the name iOS developers expect. | All |
handleNotificationPopUp() | The same as requestNotificationPermission. | All |
navigateToNotificationSettings() | Opens this app’s notification settings. | Android, iOS |
navigateToSettings() | Opens this app’s settings. | Android, iOS |
- Method
copyText(text, message?)- What it does
- Copies the text, and shows
messageas a toast.navigator.clipboard.writeTextinside a message is routed here, because a sandbox or a WebView usually refuses it. - Where
- All
- Method
call(mobileNumber)- What it does
- Opens the dialer with the number.
- Where
- All
- Method
sms(mobileNumber, message?)- What it does
- Opens the SMS composer with the number and the text.
- Where
- All
- Method
share(text)- What it does
- Opens the share sheet (on the web, where the browser has one).
- Where
- All
- Method
customAction(dataJson)- What it does
- Records the click, then hands the key-values to the app:
setClickActionListeneron Android, the delegate’sinAppClickedon iOS,inAppCampaignCustomActionin React Native,TREEBARS_ONSITE_MESSAGE_CLICKEDon the web. An object is accepted as well as a JSON string. - Where
- All
- Method
requestNotificationPermission()- What it does
- Asks for push permission, and resolves with the answer.
- Where
- All
- Method
showPushOptIn()- What it does
- The same as
requestNotificationPermission, under the name iOS developers expect. - Where
- All
- Method
handleNotificationPopUp()- What it does
- The same as
requestNotificationPermission. - Where
- All
- Method
navigateToNotificationSettings()- What it does
- Opens this app’s notification settings.
- Where
- Android, iOS
- Method
navigateToSettings()- What it does
- Opens this app’s settings.
- Where
- Android, iOS
Ours
The methods beyond the standard set.
| Method | What it does | Where |
|---|---|---|
appAction(name, payload?) | Records the click with the action’s name, then hands the app the action and its payload: the app subscribes with onAction(name, …) on every SDK, and one fired before it has is held for a few seconds. navigate is reserved, with { screen, props }. A name that is not lower-case letters, digits, dots and underscores, or a payload over 4 KB, is refused. | All |
getContext() | Resolves the locale, direction, theme, platform, safe-area insets, the screen on show, the delivery and campaign, and the bridge version. | All |
showScreen(screenId) | Goes to a screen, and records in_app_screen_viewed with how: "jump". | All |
nextScreen() | Goes to the next screen. | All |
previousScreen() | Goes back to the screen before. | All |
setState(key, value) | Keeps a value across this display’s screens. | All |
getState(key) | Reads a value setState kept. | All |
submitForm(formId?) | Checks and submits the screen’s inputs, and records in_app_form_submitted. | All |
claimReward(poolId) | Asks the server for a reward from a pool, which it decides — never the page. Until games land (H6) it resolves { ok: false, reason: "not_available" }. | All |
requestStoreReview() | Shows the store’s review prompt. | Android, iOS |
resize(height?) | Tells the host how tall the content is; the Screens runtime calls it by itself, and the shim measures when no height is given. | All |
getStoredValue(key) | Reads a value kept for this person and this campaign, on our server — so a seven-day challenge keeps its streak across days and devices. Also readable at send as {{ template.stored.<key> }}. | All |
setStoredValue(key, value) | Keeps a value for this person and this campaign (at most 32 keys, 1 KB each). null removes it. | All |
- Method
appAction(name, payload?)- What it does
- Records the click with the action’s name, then hands the app the action and its payload: the app subscribes with
onAction(name, …)on every SDK, and one fired before it has is held for a few seconds.navigateis reserved, with{ screen, props }. A name that is not lower-case letters, digits, dots and underscores, or a payload over 4 KB, is refused. - Where
- All
- Method
getContext()- What it does
- Resolves the locale, direction, theme, platform, safe-area insets, the screen on show, the delivery and campaign, and the bridge version.
- Where
- All
- Method
showScreen(screenId)- What it does
- Goes to a screen, and records
in_app_screen_viewedwithhow: "jump". - Where
- All
- Method
nextScreen()- What it does
- Goes to the next screen.
- Where
- All
- Method
previousScreen()- What it does
- Goes back to the screen before.
- Where
- All
- Method
setState(key, value)- What it does
- Keeps a value across this display’s screens.
- Where
- All
- Method
getState(key)- What it does
- Reads a value
setStatekept. - Where
- All
- Method
submitForm(formId?)- What it does
- Checks and submits the screen’s inputs, and records
in_app_form_submitted. - Where
- All
- Method
claimReward(poolId)- What it does
- Asks the server for a reward from a pool, which it decides — never the page. Until games land (H6) it resolves
{ ok: false, reason: "not_available" }. - Where
- All
- Method
requestStoreReview()- What it does
- Shows the store’s review prompt.
- Where
- Android, iOS
- Method
resize(height?)- What it does
- Tells the host how tall the content is; the Screens runtime calls it by itself, and the shim measures when no height is given.
- Where
- All
- Method
getStoredValue(key)- What it does
- Reads a value kept for this person and this campaign, on our server — so a seven-day challenge keeps its streak across days and devices. Also readable at send as
{{ template.stored.<key> }}. - Where
- All
- Method
setStoredValue(key, value)- What it does
- Keeps a value for this person and this campaign (at most 32 keys, 1 KB each).
nullremoves it. - Where
- All
Your own events
trackEvent records only an event the message declares — a data-tb-event, a trackEvent('name', …) written with its name in quotes, or a name in a manifest, <script type="application/json" id="tb-manifest">{ "events": ["quiz_answered"] }</script> for a name your script builds. At most 20 a display, and a repeat of one name within 250 ms is dropped. The campaign is attached every time, so the fifth and sixth arguments some bridges take for that are accepted here and ignored.
Examples
<button onclick="treebars.setEmailId(email.value);
treebars.trackEvent('response_submitted', {email: email.value}, {}, {}, false, true);
treebars.trackClick(1); treebars.dismissMessage()">Sign me up</button>const answer = await treebars.requestNotificationPermission();
if (answer === 'denied') treebars.showScreen('push-later');
else treebars.nextScreen();const streak = (await treebars.getStoredValue('streak')) ?? 0;
await treebars.setStoredValue('streak', streak + 1);Writing one with an agent
Everything on this page, as one file an agent can read — Claude Code, Cursor, or your own model through the Treebars connector: agents.md, with the bridge as TypeScript in treebars.d.ts. Both are built from the same tables the SDKs and publish run on, so they describe what your devices do today. Point the agent at the first and give it a brief; what it writes is checked by the same rules as publish, and becomes a draft a person applies.
Bringing a template from another platform
The only global is treebars. A template that still calls the global its own platform gave it is refused at publish, naming it, and the code view rewrites it in one click — the calls themselves do not change. Personalisation is written our way: {{ user.first_name }} for {{UserAttribute['First Name']}}, and a token inside a script is escaped for the script.
localStorage is refused inside the message's frame. Use getStoredValue and setStoredValue, which keep a value for the person in the campaign on our side.