We’ve migrated our documentation to a new site, which means some URLs have changed.
Subscriptions

Piano Composer Mobile Integration Guide

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

https://gitlab.com/piano-public/sdk/ios/package

Always

PianoC1X

https://gitlab.com/piano-public/sdk/ios/packages/c1x

Always (CX data layer)

PianoOAuth

https://gitlab.com/piano-public/sdk/ios/package

Login/registration

PianoConsents

https://gitlab.com/piano-public/sdk/ios/packages/consents

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)

Composer.Endpoint.PRODUCTION

PianoEndpoint.production

Europe

Composer.Endpoint.PRODUCTION_EUROPE

PianoEndpoint.productionEurope

Australia

Composer.Endpoint.PRODUCTION_AUSTRALIA

PianoEndpoint.productionAustralia

Asia-Pacific

Composer.Endpoint.PRODUCTION_ASIA_PACIFIC

PianoEndpoint.productionAsiaPacific

Sandbox

Composer.Endpoint.SANDBOX

PianoEndpoint.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

url

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., app://yourapp/article/12345).

contentId

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.

userToken

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).

tag / tags

Optional

Content classification tags. Used by Composer experience rules for targeting.

contentAuthor

Optional

Enriches CX content profiles with author metadata.

contentSection

Optional

Enriches CX content profiles with section metadata.

contentCreated

Optional

Content publication date in ISO 8601 format (UTC).

referrer / referer

Optional

Where the user came from. Used by Composer rules and CX attribution.

contentIsNative

Optional

Boolean flag for native vs. sponsored content.

zoneId

Optional

Zone identifier for experience targeting.

customVariable

Optional

Key-value pairs passed to Composer rules and available in templates via {{custom.variableName}}.

debug

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 calls composer.browserId(Cxense.getPersistentCookie()) on each execution.

  • After every Composer execution, sends a CX pageview event. The interceptor checks the Composer response for a cxenseCustomerPrefix field (which indicates CX is configured for your account). If present, it builds a PageViewEvent with 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), or hasActiveAccess (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 the ExperienceRequest.Builder. Watch Logcat for messages prefixed with Piano:.

  • iOS: Call .debug(true) on your PianoComposer instance. Watch the console for messages prefixed with PianoSDK.

Checklist

  • SDK initializes without errors. No crash or exception during app startup.

  • Composer executes successfully. You see an experienceExecute event (not experienceExecutionFailed). If you see mobile_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: url parameter not set on Composer requests, cxenseCustomerPrefix not 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 domStorageEnabled not set on Android WebView

C1X pageviews not appearing

cxenseCustomerPrefix not configured for your account (contact Piano), or url parameter not set on Composer request

User always treated as anonymous

userToken not being passed, or token is expired

experienceExecutionFailed

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

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

io.piano.android:composer

Core experience execution

Composer C1X

io.piano.android:composer-c1x

CX pageview tracking + recommendations

Show Template

io.piano.android:composer-show-template

Template/paywall display

Piano ID

io.piano.android:id

Authentication

Show Custom Form

io.piano.android:show-custom-form

Piano ID form display

Source: github.com/tinypass/piano-sdk-for-android

iOS

Package

Repository

Products

PianoSDK (main)

https://gitlab.com/piano-public/sdk/ios/package

PianoCommon, PianoComposer, PianoTemplate

PianoC1X

https://gitlab.com/piano-public/sdk/ios/packages/c1x

PianoC1X

PianoConsents

https://gitlab.com/piano-public/sdk/ios/packages/consents

PianoConsents

PianoOAuth Google

https://gitlab.com/piano-public/sdk/ios/packages/oauth-google

PianoOAuthGoogle

PianoOAuth Facebook

https://gitlab.com/piano-public/sdk/ios/packages/oauth-facebook

PianoOAuthFacebook

PianoAPI

https://gitlab.com/piano-public/sdk/ios/packages/api

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.

Last updated: