# Glomo Android SDK v2

Official Android SDK for integrating Glomo payment checkout flows into your native Android applications.

> Current version: **v2.0.0**. Upgrading from v1.0.0? See the [Migration Guide](/platform/sdk/android-sdk/migration). Every integration needs at least one change - the new required `onUserJourneyCompleted` callback - and the compiler points at it.
Full [Changelog](/platform/sdk/android-sdk/changelog) is also available.


## Prerequisites

Before using this SDK, you need:

- API credentials (Public Key) from your Glomo dashboard
- An order ID created via the Glomo API (or a subscription ID for subscription payments)


## System Requirements

| Requirement | Version |
|  --- | --- |
| Android SDK | minSdk 24 (Android 7.0+) |
| Compile SDK | 35 or later (enforced) |
| Target SDK | 34 to 36 (supported) |
| Kotlin | >= 2.0 |
| Java / JVM target | 17 |
| Gradle | >= 8.0 |


The AAR declares `minCompileSdk 35`, so a host app compiling against an older SDK will fail to resolve the dependency. Raise `compileSdk` to 35 or later.

## Installation

The SDK is published to Maven Central.

**Gradle (Kotlin DSL):**

```kotlin
repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation("com.glomopay:glomo-android-sdk:2.0.0")
}
```

**Gradle (Groovy DSL):**

```groovy
dependencies {
    implementation 'com.glomopay:glomo-android-sdk:2.0.0'
}
```

## Quick Start

```kotlin
import android.app.Activity
import android.util.Log
import com.glomopay.sdk.android.*

class CheckoutActivity : Activity(), GlomoPayListener {

    private var checkout: GlomoPayCheckoutHandle? = null

    fun startPayment() {
        val config = GlomoPayConfig(
            publicKey = "live_your_public_key",
            orderId = "order_your_order_id",
        )
        checkout = GlomoPaySdk.startCheckout(this, config, this)
    }

    override fun onPaymentSuccess(payload: GlomoPayPayload) {
        // Verify payload.signature on your server before fulfilling the order
        val orderId = payload.orderId
        val paymentId = payload.paymentId
        val signature = payload.signature
    }

    override fun onPaymentFailure(payload: GlomoPayPayload) {
        // The backend confirmed the payment failed. No signature is carried here
    }

    override fun onUserJourneyCompleted(payload: GlomoPayUserJourneyPayload) {
        // Required. A journey ended without a confirmed payment - today only a
        // submitted bank transfer. No money has moved: reconcile server-side
        val orderId = payload.orderId
        val reference = payload.transactionReference
    }

    override fun onSdkError(errors: List<SdkError>) {
        errors.forEach { error ->
            Log.e("Glomo", "${error.type}: ${error.message}")
        }
    }

    override fun onConnectionError(error: ConnectionError) {
        if (error.isRecoverable) {
            // Retry or show retry UI
        }
    }

    override fun onPaymentTerminate(source: TerminationSource) {
        // Checkout ended without a payment result
    }
}
```

The SDK's `AndroidManifest.xml` declares `INTERNET`, `ACCESS_NETWORK_STATE`, the checkout Activity and the upload `FileProvider` automatically. These merge into your app's manifest - no additional manifest configuration is required.

## Subscriptions Checkout

To process subscription payments, pass a `subscriptionId` instead of an `orderId`:

```kotlin
val config = GlomoPayConfig(
    publicKey = "live_your_public_key",
    subscriptionId = "sub_your_subscription_id",
)
GlomoPaySdk.startCheckout(this, config, this)
```

When `subscriptionId` is provided:

- The SDK skips order type detection and opens the Standard checkout flow
- The `subscriptionId` must start with `sub_`
- Do not pass both `orderId` and `subscriptionId` - the SDK will fire `onSdkError`


## Asynchronous Payment Journeys

Not every checkout ends in a payment the backend can confirm. When the user submits bank transfer details, the money has not moved yet and there is nothing for your server to verify - no `paymentId` and no `signature`.

**New and required in v2.0.0**, this ends the checkout through `onUserJourneyCompleted` with a `GlomoPayUserJourneyPayload`:

```kotlin
override fun onUserJourneyCompleted(payload: GlomoPayUserJourneyPayload) {
    when (payload.journeyType) {
        GlomoPayUserJourneyType.BANK_TRANSFER -> {
            // Mark the order awaiting reconciliation. Do not fulfil it
        }
    }
}
```

In v1 this arrived as `onPaymentSuccess` with `paymentId` and `signature` both null - a payment reported as received that your backend had nothing to verify against. It now reports here instead, and **no longer reaches `onPaymentSuccess`**.

One journey produces one callback. A submitted bank transfer is an ending in its own right and does not also arrive as `onPaymentSuccess` or `onPaymentTerminate`.

## Features

- **Standard, LRS, and Subscriptions** checkout flows
- **Automatic order type detection** from the order API response, with explicit failure rather than a guess
- **WebView-based secure checkout** - payment data never passes through merchant code
- **Payment callbacks** with signature verification
- **Programmatic close** - `startCheckout` returns a handle that dismisses that session. **New in v2.0.0**
- **Document uploads** - camera, gallery and file entry points for bank document fields
- **Device security compliance** - root and debugger detection for live keys
- **Self-contained error reporting** - no Sentry artifact enters your dependency graph. **New in v2.0.0**
- **Mock mode** for testing with `test_` and `mock_` key prefixes
- **Connection error handling** with recovery hints (`isRecoverable`)
- **LRS education carousel** above the secure bank flow
- **ProGuard/R8 compatible** - consumer rules included automatically


## API Reference

### GlomoPaySdk

```kotlin
object GlomoPaySdk {
    fun startCheckout(
        context: Context,
        config: GlomoPayConfig,
        listener: GlomoPayListener,
        orderType: String = "auto",
    ): GlomoPayCheckoutHandle
}
```

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `context` | `Context` | Yes | Android Activity or application context. A non-Activity context gets the new-task flag |
| `config` | `GlomoPayConfig` | Yes | Checkout configuration |
| `listener` | `GlomoPayListener` | Yes | Callback listener for payment events |
| `orderType` | `String` | No | `"auto"` (default, recommended), `"standard"`, or `"lrs"`. Auto-detects from the order API |


**Returns `GlomoPayCheckoutHandle`** as of v2.0.0. In v1 this returned nothing.

### GlomoPayCheckoutHandle

```kotlin
class GlomoPayCheckoutHandle {
    fun close()
}
```

**New in v2.0.0.** Retain the handle to dismiss that checkout programmatically - for example when the host app signs the user out. The listener then receives `onPaymentTerminate(PROGRAMMATIC)` once.

The handle controls only the session it was returned for. Calling `close()` after that session has completed does nothing. It is safe to call from any thread.

### GlomoPayConfig

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `publicKey` | `String` | Yes | Your Glomo public key. Must start with `live_`, `test_`, or `mock_` |
| `orderId` | `String?` | Conditional | Order ID (starts with `order_`). Required unless using `subscriptionId` |
| `subscriptionId` | `String?` | Conditional | Subscription ID (starts with `sub_`). Mutually exclusive with `orderId` |


Exactly one of `orderId` or `subscriptionId` must be provided. If both or neither are set, `onSdkError` fires.

### GlomoPayListener

| Callback | Parameters | Required | Description |
|  --- | --- | --- | --- |
| `onPaymentSuccess` | `payload: GlomoPayPayload` | Yes | A payment the backend confirms succeeded. Verify `signature` server-side before fulfilling |
| `onPaymentFailure` | `payload: GlomoPayPayload` | Yes | The backend confirms a payment failed. Carries no `signature` - a failure payload has never had one |
| `onUserJourneyCompleted` | `payload: GlomoPayUserJourneyPayload` | Yes | A journey ended the checkout without a confirmed payment - today only `BANK_TRANSFER`. **New and required in v2.0.0** |
| `onSdkError` | `errors: List<SdkError>` | Yes | The SDK or the order could not proceed - validation, forbidden device, or an order fault the backend answered |
| `onConnectionError` | `error: ConnectionError` | Yes | Network, DNS, SSL, HTTP or WebView failure. Check `isRecoverable` for a retry hint |
| `onPaymentTerminate` | `source: TerminationSource` | No | The checkout ended without a payment result. Default no-op |
| `onUserRefusedDevicePermissions` | `permission: String` | No | The user refused a permission the checkout asked for, such as camera. The checkout stays open. Default no-op. **New in v2.0.0** |
| `onEvent` | `name: String, payload: Map<String, Any?>` | No | **Deprecated in v2.0.0.** Diagnostic events that are subject to change and are not part of the integration contract. Default no-op |


`onPaymentTerminate`, `onUserRefusedDevicePermissions` and `onEvent` have default implementations, so you only override what you use. `onUserJourneyCompleted` deliberately has none: the order decides server-side whether bank transfers are enabled, so a default body would let an integration upgrade, keep compiling, and silently stop hearing about a journey it used to be told about.

### GlomoPayPayload

Delivered by `onPaymentSuccess` and `onPaymentFailure`.

```kotlin
data class GlomoPayPayload(
    val orderId: String,
    val paymentId: String? = null,
    val signature: String? = null,
    val rawResponse: Map<String, Any?>? = null,
)
```

**Merchant responsibility:** Always verify the `signature` on your server before fulfilling an order. Check it against each of your active secret keys, and accept it if any one matches. See [Checkout overview](/payin/checkout#steps-to-integrate-checkout) for verification instructions.

Do not expect a `signature` on `onPaymentFailure`. That field exists so a host can verify a success; a failure payload has never carried one. The page's response travels verbatim in `rawResponse`.

### GlomoPayUserJourneyPayload

Delivered by `onUserJourneyCompleted`. **New in v2.0.0.**

```kotlin
data class GlomoPayUserJourneyPayload(
    val journeyType: GlomoPayUserJourneyType,
    val orderId: String,
    val senderAccountNumber: String? = null,
    val transactionReference: String? = null,
    val status: String? = null,
    val rawResponse: Map<String, Any?>? = null,
)

enum class GlomoPayUserJourneyType { BANK_TRANSFER }
```

Deliberately not a `GlomoPayPayload`: `paymentId` and `signature` are the fields a host verifies a payment with, and neither exists for a journey. Reconcile it server-side against the order and never fulfil from it.

Journey fields are read in both camelCase and snake_case, and the page's response travels verbatim in `rawResponse`. A payload with no `orderId` is rejected rather than delivered. `GlomoPayUserJourneyType` is append-only, so an exhaustive `when` keeps compiling until a new member ships.

### SdkError

```kotlin
enum class SdkErrorType {
    VALIDATION_ERROR,
    DEVICE_FORBIDDEN,
    NETWORK_ERROR,
    UNKNOWN,
}

data class SdkError(
    val type: SdkErrorType,
    val message: String,
    val field: String? = null,
)
```

| Type | When |
|  --- | --- |
| `VALIDATION_ERROR` | Invalid public key, order ID, or subscription ID format. `field` indicates which failed |
| `DEVICE_FORBIDDEN` | Root or debugger detected on a device using a `live_` key |
| `NETWORK_ERROR` | The order could not be loaded before opening checkout |
| `UNKNOWN` | Unexpected error |


As of v2.0.0, an order fault the backend actually answered - an HTTP status, or a 2xx body the client cannot parse - is reported here only. In v1 it also delivered `onPaymentTerminate(CONNECTION_ERROR)`, which told hosts the network had failed when it had not.

### ConnectionError

```kotlin
enum class ConnectionErrorType {
    NO_INTERNET,
    DNS_FAILURE,
    TIMEOUT,
    SSL_ERROR,
    HTTP_CLIENT_ERROR,
    HTTP_SERVER_ERROR,
    WEB_RESOURCE_ERROR,
    UNKNOWN,
}

data class ConnectionError(
    val type: ConnectionErrorType,
    val message: String,
    val statusCode: Int? = null,
    val failedUrl: String? = null,
    val errorCode: Int? = null,
    val shouldAutoClose: Boolean = true,
)
```

| Type | Recoverable | Description |
|  --- | --- | --- |
| `NO_INTERNET` | Yes | Device is offline |
| `DNS_FAILURE` | No | DNS resolution failed |
| `TIMEOUT` | Yes | Request timed out |
| `SSL_ERROR` | No | SSL/TLS handshake failed |
| `HTTP_CLIENT_ERROR` | No | HTTP 4xx response |
| `HTTP_SERVER_ERROR` | Yes | HTTP 5xx response |
| `WEB_RESOURCE_ERROR` | No | WebView resource loading failed |
| `UNKNOWN` | No | Unexpected error |


`errorCode` and `shouldAutoClose` are **new in v2.0.0**. `errorCode` carries the native WebView error code where one exists. `shouldAutoClose` is `false` for an error the checkout can survive - see [Slow checkout warning](#slow-checkout-warning) - and the SDK keeps the session open in that case.

### TerminationSource

```kotlin
enum class TerminationSource {
    USER_DISMISS,
    BACK_BUTTON,
    PROGRAMMATIC,
    CONNECTION_ERROR,
}
```

| Source | When |
|  --- | --- |
| `USER_DISMISS` | The user closed the checkout from its close control |
| `BACK_BUTTON` | The user dismissed the checkout with the system Back gesture or button |
| `PROGRAMMATIC` | The host called `GlomoPayCheckoutHandle.close()`. The enum member existed in v1; v2.0.0 is the first release with a public way to reach it |
| `CONNECTION_ERROR` | A connection error ended the checkout |


## Slow checkout warning

**New in v2.0.0.** If the checkout page has not become interactive 15 seconds after the Activity opens, the SDK raises `onConnectionError` with `ConnectionErrorType.TIMEOUT` and `shouldAutoClose = false`.

This is a warning, not an ending: the checkout stays open and can still recover on its own. If your `onConnectionError` closes the checkout for every error it receives, check `shouldAutoClose` before doing so.

## Document Uploads

Bank flows that ask for documents open a chooser with **Camera**, **Gallery** and **Files** entry points. When the page asks specifically for an image capture, the camera opens directly.

- Camera capture needs the `CAMERA` permission. If the user refuses it, `onUserRefusedDevicePermissions` fires with the permission name and the checkout stays open - the bank's upload field is simply not satisfied. In v1 this was reported as an SDK error
- Camera output is written as JPEG, limited to 2048 pixels per side at quality 85
- Gallery uses the system photo picker on API 33 and later, falling back to the documents picker where it is unavailable
- Documents picked from Files are passed through as-is: not read, not re-encoded and not size-limited
- Camera captures are written to a per-checkout directory inside your app's cache directory. Opening a checkout deletes any capture left behind by an earlier one, so a crash or a kill mid-upload does not leave KYC photos on the device


## Session Lifecycle

- The checkout runs in its own Activity (fullscreen, portrait), declared in the SDK's manifest and merged automatically
- The Activity declares `configChanges` for theme, locale, font scale, density, screen size and orientation, so a dark-mode toggle, a locale change or a multi-window resize no longer ends a live payment. **New in v2.0.0** - only real process death reaches that path
- If the process is killed and the Activity is recreated, the SDK ends that session rather than silently replaying the checkout. A recreated checkout cannot resume a payment
- The listener is held for the lifetime of one checkout and released on every terminal path
- Back closes a bank overlay first where one is open, including with predictive Back; otherwise it dismisses the checkout and fires `onPaymentTerminate(BACK_BUTTON)`


## Device Security Compliance

| Scenario | Behavior |
|  --- | --- |
| `live_` key + rooted/debuggable device | `onSdkError` with `DEVICE_FORBIDDEN`. Checkout does not open |
| `live_` key + clean device | Checkout proceeds normally |
| `test_` or `mock_` key | Compliance checks skipped entirely |


## Mock Mode

| Key prefix | Mode | Description |
|  --- | --- | --- |
| `live_` | Production | Real payments. Device compliance enforced |
| `test_` | Test | Mock checkout. No real transactions. Compliance skipped |
| `mock_` | Mock | Same as `test_` |


## Input Validation

| Field | Rule |
|  --- | --- |
| `publicKey` | Length > 5. Must start with `live_`, `test_`, or `mock_` |
| `orderId` | Length > 6. Must start with `order_` |
| `subscriptionId` | Length > 4. Must start with `sub_` |


- Exactly one of `orderId` or `subscriptionId` must be provided
- Validation errors fire `onSdkError` with `VALIDATION_ERROR` and the `field` that failed


## Order Type Detection

With the default `orderType = "auto"`, the SDK resolves the flow from the order API response:

1. A non-empty `orderType` response field wins
2. Otherwise, a non-null `lrs` response field selects LRS
3. Otherwise, Standard is selected


If order detection fails, the checkout does not open and the session ends. The failure arrives through `onConnectionError` for a timeout or lost connectivity, or `onSdkError` for an HTTP status or a malformed response. **As of v2.0.0 no order type is guessed** - guessing Standard would route LRS traffic to the wrong checkout host.

Pass `"standard"` or `"lrs"` only when you intend to override detection.

## ProGuard / R8

The SDK includes consumer ProGuard rules that are applied automatically. Merchants do not need to add any ProGuard configuration, and release builds can keep shrinking, optimization and obfuscation enabled. R8 full mode is safe.

You do not need to upload your R8 mapping to Glomo. The SDK does not read your application's `sentry-debug-meta.properties` and does not attach a mapping UUID to its own diagnostics.

## Dependency Notes

**v2.0.0 removes the `io.sentry:sentry` dependency.** The SDK's own error reporting now runs over a small internal client, so no Sentry artifact enters your dependency graph and nothing conflicts with your application's own Sentry version or Gradle plugin, at any version. No public API changed.

The SDK depends on `androidx.core:core-ktx`, `androidx.appcompat:appcompat`, `androidx.webkit:webkit`, `kotlinx-coroutines-android` and a root-detection library.

## Troubleshooting

### Build fails with "object is not abstract and does not implement abstract member onUserJourneyCompleted"

Expected on upgrade from v1.0.0. Add the callback. If you do not take bank transfers, an empty body is a complete migration - it simply never fires. See the [Migration Guide](/platform/sdk/android-sdk/migration).

### Dependency resolution fails with a `minCompileSdk` error

The AAR requires `compileSdk` 35 or later. Raise it in your module's `build.gradle.kts`.

### `onSdkError` fires immediately

Check that `publicKey` format is valid and that exactly one of `orderId` / `subscriptionId` is provided.

### `DEVICE_FORBIDDEN` in development

Use a `test_` or `mock_` key prefix during development.

### `onConnectionError` with `TIMEOUT` but the checkout is still open

This is the 15-second [slow checkout warning](#slow-checkout-warning), raised with `shouldAutoClose = false`. The session is still alive. Check `shouldAutoClose` before closing the checkout from this callback.

### `onConnectionError` with `SSL_ERROR`

Ensure the device's system time is correct. Check for corporate proxy or certificate pinning issues.

### Bank transfers stopped reaching `onPaymentSuccess`

Expected in v2.0.0. They now arrive at `onUserJourneyCompleted`, because they carry no `paymentId` and no `signature` for your backend to verify. See [Asynchronous Payment Journeys](#asynchronous-payment-journeys).

### A dark-mode or locale change used to end the checkout

Fixed in v2.0.0 by declaring `configChanges` on the checkout Activity. Only real process death now ends a live session.

## Security

- Payment data is handled within a secure, isolated WebView - it never passes through the merchant's application code
- JavaScript bridge communication is limited to payment lifecycle events; no PAN, CVV, or sensitive payment data crosses the bridge
- Root and debugger detection blocks checkout on compromised devices (live keys only)
- External schemes are blocked inside bank flows, and popup URLs are validated before they open
- File access is disabled in the WebView; only `content://` URIs from the system picker are allowed
- The SDK's manifest declares `android:usesCleartextTraffic="false"`. If your application needs a different value, resolve it with the standard manifest-merger `tools` attributes
- Merchant cookies, the process-wide WebView resource cache and your application's WebView debugging preference are preserved. The SDK clears WebView state at teardown only, never at checkout start, because `CookieManager` and `WebStorage` are process-wide on Android and clearing at start would erase your app's own WebView sessions
- An exception thrown from one of your callbacks is isolated so it cannot take the checkout down, and is reported by type and stack trace only, never by message


To report a security vulnerability, email security@glomopay.com. Do not open a public GitHub issue for security reports.

## Related

- [Migration Guide (v1.0.0 to v2.0.0)](/platform/sdk/android-sdk/migration) - step-by-step upgrade instructions
- [Changelog](/platform/sdk/android-sdk/changelog) - all release notes
- [Android SDK v1 (Archived)](/platform/sdk/android-sdk/v1) - previous version documentation
- [React Native SDK](/platform/sdk/react-native-sdk) - Glomo SDK for React Native apps
- [Flutter SDK](/platform/sdk/flutter-sdk) - Glomo SDK for Flutter apps
- [Unified SDK (Web)](/platform/sdk/unified-sdk) - Glomo SDK for web apps
- [Checkout overview](/payin/checkout) - server-side checkout integration and signature verification