This guide includes CX integration as a standard part of every setup. Pageview data flowing into CX is what powers Piano's propensity models (likelihood to subscribe, likelihood to churn), audience segmentation, and content recommendations. Without it, Piano can only evaluate Composer rules in your app — it cannot learn from user behavior, build predictive scores, or optimize targeting over time. Most integration issues Piano sees in the field trace back to customers who set up Composer without the CX data layer, which leaves the platform running with incomplete data.
Unless your Piano account team has specifically told you that your account does not use CX, always include the C1X integration described in this guide.
Before You Start
You will need the following from your Piano account team:
|
Item |
Description |
|---|---|
|
Application ID (AID) |
Your publisher identifier, found in Piano Dashboard under Settings. |
|
Endpoint region |
Which Piano deployment your account is on (US, Europe, Australia, Asia-Pacific, or Sandbox). |
|
Cxense Site ID |
Identifies your site within the CX platform. This is separate from your AID and is provided by your Piano account manager. Request this before you begin your integration. |
You also need at least one Composer experience configured as a Mobile type experience in the Piano Dashboard. Mobile experiences are distinct from web experiences — a web-only experience will not execute in the SDK.
To verify mobile execution is enabled for your AID, check with your Piano account team that the composer.mobile.enabled flag is active.
1. Install the SDK
Android
Add the dependencies to your module's build.gradle.kts:
dependencies {
// Core Composer + CX integration (always include both)
implementation("io.piano.android:composer:$pianoVersion")
implementation("io.piano.android:composer-c1x:$pianoVersion")
// Template display (required if your experiences show templates/paywalls)
implementation("io.piano.android:composer-show-template:$pianoVersion")
// Piano ID (required if your experiences use login/registration)
implementation("io.piano.android:id:$pianoVersion")
}
The SDK is published on Maven Central. No additional repository configuration is needed.
iOS (Swift Package Manager)
Add the packages your integration requires. Each is a separate repository:
|
Package |
Repository URL |
When to Add |
|---|---|---|
|
PianoComposer + PianoTemplate |
|
Always |
|
PianoC1X |
|
Always (CX data layer) |
|
PianoOAuth |
|
Login/registration |
|
PianoConsents |
|
Consent management |
In Xcode, go to File > Add Package Dependencies and add each URL. Select version 2.8.16 or later.
The conversion tracking API (composer.conversion()) and edge cookie support (composer.edgeCookies) require version 2.8.0 or later of the main PianoSDK package. If you are on an older version, update before implementing sections 7 and Appendix B.
iOS (CocoaPods)
use_frameworks!
pod 'PianoComposer', '>=2.8'
pod 'PianoTemplate', '>=2.8'
pod 'PianoC1X', '>=2.8' # CX data layer (always include)
pod 'PianoOAuth', '>=2.8' # Login/registration
pod 'PianoTemplate.ID', '>=2.8' # Piano ID form templates
2. Initialize the SDK
Initialization must happen once, early in your app's lifecycle — typically in Application.onCreate() (Android) or AppDelegate.didFinishLaunching (iOS).
Use PianoC1X to initialize instead of the plain Composer init. This single step sets up both Composer and the CX data layer, wires up shared user identity between them, and registers an interceptor that automatically sends pageview events to CX after every Composer execution. Without this, your app's user behavior data will not reach the CX platform.
Android
import io.piano.android.composer.c1x.PianoC1X
import io.piano.android.composer.Composer
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
PianoC1X.init(
context = this,
siteId = "your-cxense-site-id", // Cxense Site ID, not your AID
aid = "your-piano-aid",
endpoint = Composer.Endpoint.PRODUCTION // or PRODUCTION_EUROPE, etc.
)
}
}
iOS
On iOS, initialize the Cxense SDK first, then enable the PianoC1X interceptor. After this, every PianoComposer.execute() call will automatically send a pageview event to CX.
import CxenseSDK
import PianoC1X
// In AppDelegate.didFinishLaunchingWithOptions or similar:
// 1. Initialize Cxense SDK
do {
try Cxense.initialize(withConfiguration: /* your Cxense configuration */)
} catch {
print("Cxense init error: \(error)")
}
// 2. Enable C1X interceptor
let c1xConfig = PianoC1XConfiguration(siteId: "your-cxense-site-id")
PianoC1X.enable(configuration: c1xConfig)
If you initialize with Composer.init() (Android) or skip the C1X setup entirely (iOS), Composer experiences will still execute — but no pageview data will flow into CX. This means no training data for propensity models, no LtS scores, and no CX-powered recommendations for your app users. The only scenario where you should skip C1X is if your Piano account team has explicitly confirmed your account does not use the CX platform.
Endpoint Reference
|
Region |
Android |
iOS |
|---|---|---|
|
US (default) |
|
|
|
Europe |
|
|
|
Australia |
|
|
|
Asia-Pacific |
|
|
|
Sandbox |
|
|
Your AID is tied to a specific region. Using the wrong endpoint will result in 404 errors or empty responses.
3. Execute Composer on Each Screen
Every time the user navigates to a content screen in your app, you should execute Composer. This is the equivalent of a page load on the web — it evaluates your Composer experiences and fires the appropriate events (show template, show login, meter active, etc.).
Android
import io.piano.android.composer.Composer
import io.piano.android.composer.model.ExperienceRequest
val request = ExperienceRequest.Builder()
.url("https://example.com/article/12345") // Required
.contentId("article-12345") // Required
.tag("business")
.tag("finance")
.referer("https://example.com/homepage")
.contentAuthor("Jane Smith")
.contentSection("Business")
.contentCreated("2026-03-30T12:00:00Z") // ISO 8601 UTC
.debug(BuildConfig.DEBUG)
.build()
Composer.getInstance().getExperience(request, experienceListeners) { exception ->
Log.e("Piano", "Experience error", exception)
}
iOS
import PianoComposer
let composer = PianoComposer(aid: "your-piano-aid", endpoint: .production)
.delegate(self)
.url("https://example.com/article/12345") // Required
.contentId("article-12345") // Required
.tag("business")
.tag("finance")
.referrer("https://example.com/homepage")
.contentAuthor("Jane Smith")
.contentSection("Business")
.contentCreated("2026-03-30T12:00:00Z") // ISO 8601 UTC
.debug(true)
composer.execute()
Parameter Reference
|
Parameter |
Required |
Purpose |
|---|---|---|
|
|
Yes |
The canonical URL of the content. Used by CX to match content profiles and track pageviews. Even though this is an app screen, provide the equivalent web URL. If your content doesn't have a web URL, use a consistent synthetic URL scheme (e.g., |
|
|
Yes |
A unique identifier for the content. CX can use either URL or content ID to identify content; providing both is the safest approach and is strongly recommended. |
|
|
When user is logged in |
The Piano ID access token or your external JWT. Enables Composer to evaluate user-specific rules (access checks, segment membership). |
|
|
Optional |
Content classification tags. Used by Composer experience rules for targeting. |
|
|
Optional |
Enriches CX content profiles with author metadata. |
|
|
Optional |
Enriches CX content profiles with section metadata. |
|
|
Optional |
Content publication date in ISO 8601 format (UTC). |
|
|
Optional |
Where the user came from. Used by Composer rules and CX attribution. |
|
|
Optional |
Boolean flag for native vs. sponsored content. |
|
|
Optional |
Zone identifier for experience targeting. |
|
|
Optional |
Key-value pairs passed to Composer rules and available in templates via |
|
|
Optional |
Enables verbose SDK logging. |
When PianoC1X is initialized (as described in section 2), the SDK automatically sends a pageview event to CX after every successful Composer execution. You do not need to send Cxense pageview events separately. In fact, you must not send your own Cxense pageview events on screens where Composer runs — this would create duplicate pageviews. This automatic tracking is what feeds user behavior data into propensity models and audience segments.
4. Handle Composer Events
Composer experiences return events that your app must handle. The most common events are described below.
Android
Register event listeners when calling getExperience():
import io.piano.android.composer.*
import io.piano.android.composer.model.events.*
val experienceListeners: Collection<EventTypeListener<out EventType>> = listOf(
ExperienceExecuteListener { event ->
// Fires on every successful execution. Use for logging/debugging.
},
ShowTemplateListener { event ->
// A paywall, banner, or other template should be displayed.
ShowTemplateController.show(activity, event)
},
ShowLoginListener { event ->
// The experience requires the user to log in.
// Trigger your login flow here.
},
MeterListener { event ->
// A pageview meter event (active or expired).
val meterState = event.eventData.state
},
UserSegmentListener { event ->
// User segment evaluation result.
val matched = event.eventData.state == UserSegment.UserSegmentState.TRUE
},
ShowRecommendationsListener { event ->
// Display content recommendations (see section 6).
},
ExperienceExecutionFailedListener { event ->
Log.e("Piano", "Experience failed: ${event.eventData}")
}
)
iOS
Implement the PianoComposerDelegate protocol:
extension MyViewController: PianoComposerDelegate {
func experienceExecute(composer: PianoComposer, event: XpEvent,
params: ExperienceExecuteEventParams?) {
// Fires on every successful execution.
}
func showTemplate(composer: PianoComposer, event: XpEvent,
params: ShowTemplateEventParams?) {
guard let params else { return }
PianoShowTemplateController(params: params).show()
}
func showLogin(composer: PianoComposer, event: XpEvent,
params: ShowLoginEventParams?) {
// Trigger your login flow.
PianoID.shared.signIn()
}
func meterActive(composer: PianoComposer, event: XpEvent,
params: PageViewMeterEventParams?) {
// Meter is active (user still has free views).
}
func meterExpired(composer: PianoComposer, event: XpEvent,
params: PageViewMeterEventParams?) {
// Meter has expired (user has used all free views).
}
func showRecommendations(composer: PianoComposer, event: XpEvent,
params: ShowRecommendationsEventParams?) {
// Display content recommendations (see section 6).
}
func composerExecutionCompleted(composer: PianoComposer) {
// All events have been fired.
}
}
5. Set Up Piano ID (Authentication)
If your Composer experiences use login-gated rules (Show Login, access checks, user segments), you need Piano ID integration. After the user signs in, pass the access token to Composer so it can evaluate user-specific rules.
Android
// In Application.onCreate(), after Composer init:
PianoId.init(PianoId.ENDPOINT_PRODUCTION, "your-piano-aid")
// Trigger sign-in (e.g., from ShowLoginListener):
PianoId.signIn()
// After successful sign-in, pass the token to Composer:
Composer.getInstance().userToken(pianoIdToken.accessToken)
iOS
// Configure Piano ID (in AppDelegate or early init):
PianoID.shared.aid = "your-piano-aid"
PianoID.shared.endpoint = .production
PianoID.shared.delegate = self
// Also register the URL scheme: io.piano.id.<your-aid-lowercase>
// In Info.plist > URL Types, or in your Xcode project settings.
// Handle the callback in your AppDelegate:
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return PianoIDApplicationDelegate.shared.application(app, open: url, options: options)
}
// Trigger sign-in (e.g., from showLogin delegate):
PianoID.shared.signIn()
// After sign-in, pass the token when creating Composer instances:
PianoComposer(aid: "your-piano-aid", endpoint: .production)
.userToken(pianoIdToken.accessToken)
// ... other params
.execute()
Using an External Identity Provider
If you authenticate users through your own system (not Piano ID), pass your JWT access token as the userToken parameter. Piano will verify the login session using your configured external user provider.
6. Display Content Recommendations
If your Composer experiences include a Show Recommendations card, the SDK can display CX-powered content recommendations. These are driven by the same CX pageview data that feeds propensity models — another reason C1X initialization (section 2) is essential.
Android — Using the Built-in Controller
The simplest approach uses the built-in dialog:
ShowRecommendationsListener { event ->
ShowRecommendationsController(event).show(activity)
}
Android — Custom Display
For more control (e.g., displaying recommendations in a RecyclerView), load the recommendations yourself:
ShowRecommendationsListener { event ->
CxenseSdk.getInstance().loadWidgetRecommendations(
widgetId = event.eventData.widgetId,
experienceId = event.eventExecutionContext.experienceId,
callback = object : LoadCallback<List<WidgetItem>> {
override fun onSuccess(data: List<WidgetItem>) {
val trackingId = event.eventExecutionContext.trackingId
// Display the items in your UI
adapter.submitRecommendations(trackingId, data)
// Report that recommendations were shown
Composer.getInstance().trackRecommendationsDisplay(trackingId)
}
override fun onError(throwable: Throwable) {
Log.e("Piano", "Recommendations error", throwable)
}
}
)
}
When the user taps a recommendation, track the click:
CxenseSdk.getInstance().trackClick(item, object : LoadCallback<Unit> {
override fun onSuccess(data: Unit) {
Composer.getInstance().trackRecommendationsClick(trackingId, item.url)
}
override fun onError(throwable: Throwable) { /* handle */ }
})
iOS — Using the Built-in Controller
func showRecommendations(composer: PianoComposer, event: XpEvent,
params: ShowRecommendationsEventParams?) {
guard let params else { return }
PianoC1X.recommendations(params: params).show()
}
All recommendation image and tracking URLs must use HTTPS. iOS blocks HTTP requests by default.
7. Track Conversions
Conversion tracking tells Piano that a user completed a desired action — subscribing, registering, or hitting a custom conversion goal. This data feeds into CX propensity models and Composer analytics.
The SDK provides four conversion methods. On iOS, they are accessed through the PianoComposerConversion class. The SDK automatically populates many parameters from the current Composer instance (AID, user token, browser ID, URL, page title, referrer, content metadata, tags, and pageview ID), so you only need to provide the conversion-specific fields.
iOS
func showTemplate(composer: PianoComposer, event: XpEvent,
params: ShowTemplateEventParams?) {
guard let params else { return }
let conversion = composer.conversion()
// Standard conversion (e.g., subscription purchase)
conversion.logConversion(
trackingId: params.trackingId,
termId: "<TERM_ID>",
termName: "<TERM_NAME>"
) { error in
if let error { print("Conversion error: \(error)") }
}
// Micro-conversion (e.g., newsletter signup, content share)
conversion.logMicroConversion(
trackingId: params.trackingId,
eventGroupId: "<EVENT_GROUP_ID>"
) { error in
if let error { print("Micro-conversion error: \(error)") }
}
// Registration conversion
conversion.createRegistrationConversion(
termId: "<TERM_ID>"
) { termConversion, error in
// Handle result
}
// External verified conversion (for server-validated purchases)
conversion.externalVerifiedCreate(
termId: "<TERM_ID>",
fields: "{\"key\":\"value\"}", // JSON string with custom fields
trackingId: params.trackingId // Optional but recommended
) { termConversion, error in
// Handle result
}
}
Android
On Android, conversion tracking is available through the Piano API client. Refer to the Piano API documentation for the current method signatures and parameters.
Where tracking_id Comes From
The trackingId is generated by Composer for each experience execution. You receive it in the event's execution context:
-
iOS:
event.eventExecutionContext?.trackingId -
Android:
event.eventExecutionContext.trackingId
Always capture and pass this value when logging conversions — it links the conversion back to the specific experience execution that led to it.
8. How Identity Flows Between Composer and CX
Understanding how user identity is shared between Composer and CX is important for debugging and for ensuring your propensity models receive complete data.
When you initialize with PianoC1X (Android) or PianoC1X.enable() (iOS), the SDK does the following automatically:
-
Sets the Composer browserId to the Cxense persistent cookie. This means the same user identifier is sent to both Composer and CX. On Android, this happens via
browserIdProvider { cxenseSdk.defaultUserId }. On iOS, the interceptor callscomposer.browserId(Cxense.getPersistentCookie())on each execution. -
After every Composer execution, sends a CX pageview event. The interceptor checks the Composer response for a
cxenseCustomerPrefixfield (which indicates CX is configured for your account). If present, it builds aPageViewEventwith the URL/content ID, referrer, user state, and external user ID, then reports it to the Cxense SDK. -
Computes a user state for CX. The interceptor maps the Composer response into one of three states:
anon(no user ID),registered(user ID present but no active access), orhasActiveAccess(user ID present with at least one active entitlement). This user state is sent as a custom parameter on the CX pageview event.
If this chain is working correctly, you should see pageview events appearing in your CX dashboard with matching browser IDs, and LtS scores should begin populating for your users.
Do not set browserId manually when using C1X integration. The C1X interceptor handles this. Setting it yourself would break the identity link between Composer and CX.
9. Verify the Integration
Enable Debug Logging
-
Android: Pass
.debug(true)on theExperienceRequest.Builder. Watch Logcat for messages prefixed withPiano:. -
iOS: Call
.debug(true)on yourPianoComposerinstance. Watch the console for messages prefixed withPianoSDK.
Checklist
-
SDK initializes without errors. No crash or exception during app startup.
-
Composer executes successfully. You see an
experienceExecuteevent (notexperienceExecutionFailed). If you seemobile_experience_not_enabled, contact your Piano account team to enable mobile execution for your AID. -
Templates display correctly. If your experience has a Show Template card, the paywall or banner renders in a WebView.
-
Login flow triggers. If your experience has a Show Login card, the login UI appears.
-
CX pageviews appear. In the CX dashboard, verify that pageviews are arriving with the correct site ID, URLs, and user states. If pageviews are missing, the most common causes are:
urlparameter not set on Composer requests,cxenseCustomerPrefixnot configured for your account (contact Piano), or C1X not initialized at startup. -
Conversions are recorded. After triggering a conversion, verify it appears in Piano Dashboard under Reports > Conversion.
-
LtS scores populate. After sufficient pageview volume (typically a few days), check that users are being assigned to LtS segments. If scores remain empty despite pageview volume, verify that conversion events are also flowing (propensity models need both pageviews and conversions as training signals).
Common Issues
|
Symptom |
Likely Cause |
|---|---|
|
Empty response, no events fire |
Mobile experience not enabled for your AID, or experience type is "Site – Pageview" instead of "Mobile" |
|
404 from Composer endpoint |
AID/endpoint region mismatch |
|
Template shows blank WebView |
Endpoint mismatch between SDK config and template host, or |
|
C1X pageviews not appearing |
|
|
User always treated as anonymous |
|
|
|
Check the error params. Common causes: invalid AID, network issues, unsupported card type (e.g., Show Payment or Run JS in a mobile experience) |
Appendix A: Supported Composer Cards on Mobile
Not all Composer cards are available in mobile experiences. Using an unsupported card will cause the entire experience to be discarded.
|
Card |
Supported |
|---|---|
|
Pageview Meter |
✅ Yes |
|
Show Template |
✅ Yes |
|
Show Login |
✅ Yes |
|
Show Form (Piano ID) |
✅ Yes |
|
Show Recommendations (C1X) |
✅ Yes |
|
Set Response Variable |
✅ Yes |
|
Non-site Action |
✅ Yes |
|
Segment Users |
✅ Yes |
|
A/B Test |
✅ Yes |
|
Show Payment |
❌ No |
|
Run JavaScript |
❌ No |
|
Scroll Depth |
❌ No |
Appendix B: Edge Cookie Support
If your architecture involves a server-side edge layer (CDN integration), you can pass edge cookies to and from Composer.
Passing Edge Cookies to Composer
iOS:
composer.edgeResult(EdgeResult(xbc: "<XBC>", tbc: "<TBC>", pcer: "<PCER>"))
Android:
ExperienceRequest.Builder()
.edgeResult(EdgeResult(tbc, xbc, pcer))
.build()
Reading Edge Cookies After Execution
iOS:
func experienceExecute(composer: PianoComposer, event: XpEvent,
params: ExperienceExecuteEventParams?) {
let cookies = composer.edgeCookies.toMap()
// Returns: ["__xbc": "...", "__tbc": "...", "__tac": "..."]
}
Appendix C: Package and Dependency Reference
Android
|
Artifact |
Maven Coordinates |
Purpose |
|---|---|---|
|
Composer |
|
Core experience execution |
|
Composer C1X |
|
CX pageview tracking + recommendations |
|
Show Template |
|
Template/paywall display |
|
Piano ID |
|
Authentication |
|
Show Custom Form |
|
Piano ID form display |
Source: github.com/tinypass/piano-sdk-for-android
iOS
|
Package |
Repository |
Products |
|---|---|---|
|
PianoSDK (main) |
|
PianoCommon, PianoComposer, PianoTemplate |
|
PianoC1X |
|
PianoC1X |
|
PianoConsents |
|
PianoConsents |
|
PianoOAuth Google |
|
PianoOAuthGoogle |
|
PianoOAuth Facebook |
|
PianoOAuthFacebook |
|
PianoAPI |
|
PianoAPI |
Source: gitlab.com/piano-public/sdk/ios
Platform Requirements
|
Platform |
Minimum Version |
|---|---|
|
iOS |
12.0+ |
|
tvOS |
12.0+ |
|
Android |
API 19 (Android 4.4+) |
|
Xcode |
15.3 |
|
Swift |
5.10 |
All code examples in this guide use Kotlin for Android and Swift for iOS. Build configuration snippets use the native format for each tool (Gradle Kotlin DSL for Android, Ruby for CocoaPods). If your project uses Java or Objective-C, the APIs are the same — adjust the syntax accordingly.