Android SDK · v1.0.0-beta10

MatchPass SDK

Add M-Pesa pay-per-view passes to any Android app in under an hour. One composable. Zero payment infrastructure. Users pay with their phone — your content unlocks instantly.


How it works

MatchPass sits between your content and your users. When a user taps locked content, show the Paywall composable. It handles everything — phone verification, M-Pesa STK push, pass issuing, and expiry. Your code only needs to respond to onAccessGranted.

User taps locked content
        │
        ▼
MatchPassSDK.Paywall()
        │
        ├─ Existing pass? ──────────────────────► onAccessGranted (silent)
        │
        ├─ Phone known? ─── Yes ───────────────► Confirm screen
        │                    │                         │
        │                    No                        ▼
        │                    │                  M-Pesa STK Push sent
        │                    ▼                         │
        │              Phone entry             User enters PIN on phone
        │                    │                         │
        │              OTP verification        Pass issued automatically
        │                                             │
        └─────────────────────────────────────────────▼
                                              onAccessGranted(grant)

Installation

1. Add JitPack to settings.gradle.kts

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

2. Add the dependency

// app/build.gradle.kts
dependencies {
    implementation("com.github.fatahrez:MatchPassAndroidSDK:1.0.0-beta10")
}
Note: Requires minSdk 24, compileSdk 35, and Jetpack Compose BOM 2024.09.00+.

Quick Start

1. Initialise once in Application.onCreate()

Get your API key from staging.b2b.matchpass.africa. Register as an operator — your key is generated instantly and shown once.

You pick a backend with environment, not a URL — the SDK resolves the base URL itself, so there is nothing to mistype. PRODUCTION is the default.

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        MatchPassSDK.init(
            context = this,
            config = MatchPassConfig(
                apiKey      = "YOUR_API_KEY",   // from staging.b2b.matchpass.africa
                environment = MatchPassEnvironment.STAGING,  // PRODUCTION is the default
                debug       = BuildConfig.DEBUG,
            ),
        )
    }
}

// Or the equivalent fluent form
MatchPassSDK.Builder(this)
    .apiKey("YOUR_API_KEY")
    .environment(MatchPassEnvironment.STAGING)
    .debug(BuildConfig.DEBUG)
    .initialize()

2. Show the paywall when a user taps locked content

// In any composable — show when a user taps locked content
var showPaywall by remember { mutableStateOf(false) }

if (showPaywall) {
    MatchPassSDK.Paywall(
        content = MatchPassContent(
            id            = "arsenalmancity01jul26",
            title         = "Arsenal vs Manchester City",
            price         = "299",
            currency      = "KSh",
            durationHours = 4,
            contentType   = ContentType.MATCH,
        ),
        userPhone       = sessionPhone,          // null → SDK asks; set → skips OTP
        onAccessGranted = { grant ->
            showPaywall = false
            startStream(grant.token)             // pass token to your streaming backend
        },
        onDismiss = { showPaywall = false },
    )
}

3. Check access without UI (for lock icons)

// Use this to show a lock icon or play button on content cards — no UI shown
LaunchedEffect(content.id) {
    when (val result = MatchPassSDK.checkAccess(context, content)) {
        is AccessResult.Granted      -> showPlayButton(result.grant.token)
        is AccessResult.Expired      -> showRenewButton()
        is AccessResult.NotPurchased -> showLockIcon()
        // Transient failure. The SDK does NOT clear a cached pass here, so
        // prefer keeping the viewer where they are over locking them out.
        is AccessResult.Error        -> keepCurrentState()
    }
}

Content Types

Pass the right ContentType and the SDK picks the correct validation frequency, cache TTL, and pass label automatically.

ContentTypeUse caseCache TTLPass label
MATCHLive sport, single event5 minGame Pass
CHANNELLive channel, time-limited60 sChannel Pass
SEASONFull TV season, rewatch24 hSeason Pass
MOVIEMovie, own permanently30 daysOwn it
EVENTTicketed one-off — comedy, concert5 minEvent Pass

Content IDs

Content IDs are how MatchPass links a pass to a piece of content. You own the ID — pick something human-readable and date-stamped for live events so the same fixture in a future season gets a new ID and a fresh pass.

// ✅ Recommended — unique per fixture and date
id = "arsenalmancity01jul26"
id = "chiefspirates03jul26"
id = "boxing-riyadhseason05jul26"

// ✅ Stable IDs for channels and series (content repeats)
id = "ch-supersport1"
id = "series-shaka-ilembe-s1"

// ❌ Avoid — generic IDs cause pass collisions across seasons
id = "match-001"
Note: The backend creates the content record automatically on first purchase — you never need to pre-register content.

API Reference

MatchPassSDK

MethodReturnsDescription
init(context, config)UnitInitialise the SDK. Call once from Application.onCreate().
Builder(context)BuilderFluent alternative to init(). Finish with .initialize().
Paywall(...)Unit (Composable)Show the full purchase flow. Handles phone, OTP, M-Pesa, and pass issuing.
Login(...)Unit (Composable)Standalone phone OTP screen. Call before browsing if your app requires login.
checkAccess(...)AccessResult (suspend)Validate access silently. Use for lock icons and stream gates.
hasAccess(...)Boolean (suspend)Shorthand for checkAccess when you only need yes or no.
getMyPasses(...)List<MatchPassOwnedItem> (suspend)Everything this viewer owns. Use to build a library screen.
setPhone(...)UnitPre-set a verified phone from your own auth — skips OTP in the paywall.
getStoredPhone(...)String?Read the verified phone stored on this device.
signOut(...)UnitClear the session. Does NOT revoke passes.
getExpiresAt(...)Long?Epoch-ms expiry for a content pass. Use for countdown timers.

MatchPassContent

MatchPassContent(
    id            = "arsenalmancity01jul26",  // your stable content identifier
    title         = "Arsenal vs Man City",
    price         = "299",                    // string, in the currency below
    currency      = "KES",                    // default "KES"
    durationHours = 4,                        // how long the pass is valid after purchase
    thumbnailUrl  = null,                     // optional image behind the paywall panels
    contentType   = ContentType.MATCH,        // MATCH | CHANNEL | SEASON | MOVIE | EVENT
    category      = null,                     // optional — drives pricing policy lookup
    policy        = PassPolicy.forType(ContentType.MATCH),  // override cache TTL / copy
)

AccessResult

SubtypeFieldsMeaning
Grantedgrant: MatchPassGrantValid pass — present grant.token to your streaming backend
ExpiredexpiredAt: StringPass found but expired — show renewal CTA
NotPurchased—No pass exists — show paywall
Errorexception: MatchPassExceptionNetwork or server error. Any cached pass is left intact — don't lock the viewer out over a transient failure

Session management

// Read the stored phone (null if not logged in)
val phone: String? = MatchPassSDK.getStoredPhone(context)

// Clear session — does NOT revoke passes
MatchPassSDK.signOut(context)

// Get expiry epoch-ms for a specific piece of content (for countdown timers)
val expiresAt: Long? = MatchPassSDK.getExpiresAt(context, contentId)

Pre-set phone from your own auth

If your app has its own login system, pass the verified phone to MatchPass so the paywall skips its OTP step.

// After your own login succeeds, hand the verified phone to MatchPass
MatchPassSDK.setPhone(context, verifiedPhone)

// The next Paywall() call will skip OTP and go straight to M-Pesa

Pass Restore

When a user reinstalls or switches devices, the SDK automatically checks the server for an existing active pass before showing the paywall. If found, onAccessGranted fires silently — no re-purchase required. This works automatically when the user's phone is known.


Debug & Local Dev

Point the SDK at staging and turn on request logging. debug enables OkHttp request/response logging and nothing else — it does not change how OTPs are delivered.

MatchPassSDK.init(
    context = this,
    config = MatchPassConfig(
        apiKey      = "your-staging-key",
        environment = MatchPassEnvironment.STAGING,
        debug       = true,   // OkHttp request/response logging
    ),
)
Note: To see the OTP on screen instead of waiting for a message, set your operator's OTP channel to on_screen in the dashboard. That is a server-side setting on your account, independent of the client build.

Ready to integrate?

Register as an operator to get your API key and start issuing passes.

Create operator account →