A drop-in payment UI SDK for Android that lets you accept payments through Xendit with minimal integration effort. Present a fully featured payment sheet in just a few lines of code.
- Pre-built payment sheet — Full payment UI with channel selection, form validation, and error handling out of the box.
- 3DS & redirect handling — Built-in WebView for 3D Secure challenges and redirect-based payment flows.
- Customizable appearance — Configure colors, fonts, and corner radius to match your brand.
- Jetpack Compose native — Built entirely with Material 3 and modern Compose components.
- Secure by default — Client-side encryption of sensitive card data using ECDH + AES-GCM.
| Requirement | Minimum |
|---|---|
| Android | API 26 (Android 8.0) |
| Compile SDK | 37 |
| Kotlin | 2.4+ |
| Compose Bom | 2026.05.01+ |
import co.xendit.components.XenditComponents
import co.xendit.components.data.model.XenditPaymentResult
// 1. Initialize once at app startup (Application class or main Activity)
XenditComponents.initialize(appearance = XenditAppearance())
// 2. Present the payment sheet from a ComponentActivity
XenditComponents.present(
activity = this,
componentsSdkKey = "<your_components_sdk_key>"
) { result ->
when (result) {
is XenditPaymentResult.Success ->
println("Payment succeeded: ${result.paymentRequestId}")
is XenditPaymentResult.Failed ->
println("Payment failed: ${result.error.message}")
is XenditPaymentResult.Canceled ->
println("Session canceled")
is XenditPaymentResult.Expired ->
println("Session expired")
is XenditPaymentResult.Dismissed ->
println("User dismissed")
}
}import co.xendit.components.XenditComponents;
import co.xendit.components.data.model.XenditPaymentResult;
import co.xendit.components.ui.style.XenditAppearance;
// 1. Initialize once at app startup
XenditComponents.INSTANCE.initialize(new XenditAppearance(), null);
// 2. Present the payment sheet from a ComponentActivity
XenditComponents.INSTANCE.present(
this,
"<your_components_sdk_key>",
null,
result -> {
if (result instanceof XenditPaymentResult.Success) {
XenditPaymentResult.Success success = (XenditPaymentResult.Success) result;
System.out.println("Paid: " + success.getPaymentRequestId());
} else if (result instanceof XenditPaymentResult.Failed) {
XenditPaymentResult.Failed failed = (XenditPaymentResult.Failed) result;
System.out.println("Error: " + failed.getError().getMessage());
}
}
);The components_sdk_key is obtained from the Create Session API response on your backend.
Merchants can control which payment methods are shown in the payment sheet by passing merchantPreferredPaymentMethod.
The SDK currently supports filtering to these payment methods only:
XenditComponentsPaymentType.CARDSXenditComponentsPaymentType.EWALLETXenditComponentsPaymentType.QR_CODE
Use this when you want the same payment method preference for every present() call.
import co.xendit.components.XenditComponents
import co.xendit.components.XenditComponentsPaymentType
import co.xendit.components.ui.style.XenditAppearance
XenditComponents.initialize(
appearance = XenditAppearance(),
merchantPreferredPaymentMethod = listOf(
XenditComponentsPaymentType.CARDS,
XenditComponentsPaymentType.EWALLET
)
)Use this when the payment methods should vary depending on the checkout flow.
import co.xendit.components.XenditComponents
import co.xendit.components.XenditComponentsPaymentType
XenditComponents.present(
activity = this,
componentsSdkKey = "<your_components_sdk_key>",
merchantPreferredPaymentMethod = listOf(
XenditComponentsPaymentType.QR_CODE
)
) { result ->
// handle result
}import java.util.Arrays;
import java.util.Collections;
import co.xendit.components.XenditComponents;
import co.xendit.components.XenditComponentsPaymentType;
import co.xendit.components.ui.style.XenditAppearance;
XenditComponents.INSTANCE.initialize(
new XenditAppearance(),
Arrays.asList(
XenditComponentsPaymentType.CARDS,
XenditComponentsPaymentType.EWALLET
)
);
XenditComponents.INSTANCE.present(
this,
"<your_components_sdk_key>",
Collections.singletonList(XenditComponentsPaymentType.QR_CODE),
result -> {
// handle result
}
);Note: Although
XenditComponentsPaymentTypeincludes other enum values, onlyCARDS,EWALLET, andQR_CODEare currently supported for merchant preference in the Android SDK. Unsupported values are ignored, and if no supported preference remains, the SDK falls back to the supported payment methods available in the session.
Use XenditAppearance to match the payment sheet to your app's brand. All color properties have built-in defaults — pass only the values you want to override.
| Property | Type | Default | Description |
|---|---|---|---|
fontFamily |
FontFamily? |
null |
Custom font family for all text. Falls back to the bundled Inter font. |
colorPrimary |
Color |
#0052FF |
Primary CTA color — pay button, selected states, checkmarks. |
colorText |
Color |
#1A1C1E |
Main body text color. |
colorTextSecondary |
Color |
#6B7280 |
Subtitle and caption text color. |
colorTextPlaceholder |
Color |
#9CA3AF |
Placeholder text inside input fields. |
colorDisabled |
Color |
#E5E7EB |
Background of disabled buttons and controls. |
colorDanger |
Color |
#BA1A1A |
Error messages and invalid field borders. |
colorBorder |
Color |
#E6E6E6 |
Input field outlines and dividers. |
colorBackground |
Color |
#FFFFFF |
Sheet and page background. |
qrForegroundColor |
Color |
#000000 |
Tint of the container shown around the QR code image. |
qrBackgroundColor |
Color |
#FFFFFF |
Background of the container box behind the QR code image. |
borderRadius |
Dp |
8.dp |
Corner radius for buttons, fields, and cards. |
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import co.xendit.components.XenditComponents
import co.xendit.components.ui.style.XenditAppearance
val customFont = FontFamily(
Font(R.font.your_font_regular, FontWeight.Normal, FontStyle.Normal),
Font(R.font.your_font_medium, FontWeight.Medium, FontStyle.Normal),
Font(R.font.your_font_semibold, FontWeight.SemiBold, FontStyle.Normal),
Font(R.font.your_font_bold, FontWeight.Bold, FontStyle.Normal)
)
val appearance = XenditAppearance(
fontFamily = customFont,
colorPrimary = Color(0xFF0052FF),
colorText = Color(0xFF1A1C1E),
colorTextSecondary = Color(0xFF6B7280),
colorTextPlaceholder = Color(0xFF9CA3AF),
colorDisabled = Color(0xFFE5E7EB),
colorDanger = Color(0xFFBA1A1A),
colorBorder = Color(0xFFE6E6E6),
colorBackground = Color(0xFFFFFFFF),
qrForegroundColor = Color(0xFF000000),
qrBackgroundColor = Color(0xFFFFFFFF),
borderRadius = 12.dp
)
XenditComponents.initialize(appearance = appearance)
XenditComponents.present(
activity = this,
componentsSdkKey = "<your_components_sdk_key>"
) { result ->
when (result) {
is XenditPaymentResult.Success ->
println("Payment succeeded: ${result.paymentRequestId}")
is XenditPaymentResult.Failed ->
println("Payment failed: ${result.error.message}")
is XenditPaymentResult.Canceled ->
println("Session canceled")
is XenditPaymentResult.Expired ->
println("Session expired")
is XenditPaymentResult.Dismissed ->
println("User dismissed")
}
}import androidx.compose.ui.graphics.Color;
import androidx.compose.ui.text.font.Font;
import androidx.compose.ui.text.font.FontFamily;
import androidx.compose.ui.text.font.FontWeight;
import androidx.compose.ui.unit.DpKt;
import co.xendit.components.XenditComponents;
import co.xendit.components.data.model.XenditPaymentResult;
import co.xendit.components.ui.style.XenditAppearance;
import java.util.Arrays;
FontFamily customFont = new FontFamily(Arrays.asList(
new Font(R.font.your_font_regular, FontWeight.Companion.getNormal(), FontStyle.Companion.getNormal()),
new Font(R.font.your_font_semibold, FontWeight.Companion.getSemiBold(), FontStyle.Companion.getNormal()),
new Font(R.font.your_font_bold, FontWeight.Companion.getBold(), FontStyle.Companion.getNormal())
));
XenditAppearance appearance = new XenditAppearance(
customFont,
new Color(0xFF0052FFL), // colorPrimary
new Color(0xFF1A1C1EL), // colorText
new Color(0xFF6B7280L), // colorTextSecondary
new Color(0xFF9CA3AFL), // colorTextPlaceholder
new Color(0xFFE5E7EBL), // colorDisabled
new Color(0xFFBA1A1AL), // colorDanger
new Color(0xFFE6E6E6L), // colorBorder
new Color(0xFFFFFFFFL), // colorBackground
new Color(0xFF000000L), // qrForegroundColor
new Color(0xFFFFFFFFL), // qrBackgroundColor
DpKt.getDp(12) // borderRadius
);
XenditComponents.INSTANCE.initialize(appearance);
XenditComponents.INSTANCE.present(
this,
"<your_components_sdk_key>",
null,
result -> {
if (result instanceof XenditPaymentResult.Success) {
String id = ((XenditPaymentResult.Success) result).getPaymentRequestId();
System.out.println("Paid: " + id);
} else if (result instanceof XenditPaymentResult.Failed) {
String msg = ((XenditPaymentResult.Failed) result).getError().getMessage();
System.out.println("Error: " + msg);
}
}
);Tip:
XenditComponents.initialize()can be called multiple times. Each call replaces the active appearance for all subsequentpresent()calls, allowing runtime theme switching without restarting the session.
XenditComponents.dismiss()Calling dismiss() triggers the onPaymentResult callback with XenditPaymentResult.Canceled and removes the payment UI from the activity.
To ensure the payment UI is reliable and to identify regressions, fraud patterns, and performance issues quickly, the SDK sends a small, best-effort stream of in-session lifecycle events to Xendit's telemetry endpoint. This telemetry is session-scoped and non-transactional — losing events on process death is acceptable, and no data is persisted to disk on the device.
Collected:
- Session identifiers issued by Xendit:
payment_session_idandsession_auth_id(not user emails, names, or phone numbers) - UI lifecycle event types:
CHECKOUT_LOADED,CHECKOUT_CHANNEL,CHECKOUT_CHANNEL_FORM_INPUT,CHECKOUT_ATTEMPT_BEGIN,CHECKOUT_ATTEMPT,CHECKOUT_ACTION_BEGIN,CHECKOUT_ACTION_CLOSE,CHECKOUT_DIGITAL_WALLET_BEGIN/CLOSE,CHECKOUT_ACTION_COPY_TEXT,CHECKOUT_END,CHECKOUT_PENDING,CHECKOUT_ABANDON - Static, non-PII diagnostic context: Xendit payment channel codes (e.g.
BCA,SHOPEEPAY), payment-method group names (cards,ewallet,qr_code), static field labels (card_number,expiry,cvv,phone,name— never the typed value), error codes, validation keys, and status keys - Event ordering metadata (
event_id,parent_event_id) and wall-clock timestamps - Xendit Payment Request ID and Payment Token ID once set
Never collected in telemetry:
- ❌ Card PAN (full or masked), CVV/CVC, or expiry digits typed by the user
- ❌ Any value the user types into a form field — only the static
field_namelabel is tracked - ❌ Billing name, email, phone number, or address text
- ❌ Android Advertising ID (AAID/GAID), precise location, or device fingerprint
- ❌ Any cross-app or cross-site tracking identifiers
| Trigger | When |
|---|---|
| Time-based flush | Every 5 seconds if there are pending events |
| Batch-based flush | When the in-memory queue reaches 25 events |
| Background / screen exit | Activity onStop / onDestroy and ProcessLifecycleOwner onStop |
| Memory pressure | TRIM_MEMORY_BACKGROUND discards the queue (no on-disk persistence) |
| Debug Build | Release Build | |
|---|---|---|
| Logcat prints of telemetry payloads | ✅ Enabled by default (via BuildConfig.DEBUG) |
❌ Completely silenced |
| OkHttp Profiler interceptor | ✅ Attached for Android Studio Profiler visibility | ❌ Not attached |
| Network transmission (actual flush) | ✅ Sends data (test environment) | ✅ Sends data (production telemetry — required for reliability) |
Privacy & compliance note. Full Play-Data-Safety-ready disclosure, exact field-by-field schema, and source-code cross-references for every telemetry value are documented in PRIVACY.md — Performance Telemetry. Use that section together with your legal counsel to complete your Google Play Data Safety form and your app's privacy policy.
Add Maven Central to your settings.gradle.kts if not already present:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}Then add the dependency to your app module's build.gradle.kts:
dependencies {
implementation("co.xendit:components:1.2.0")
}dependencies {
implementation 'co.xendit:components:1.2.0'
}Before you offer Google Pay in production, you must register and approve your Android app with Google Pay using the exact production package name that will be distributed to end users.
If your app is not registered and approved for Google Pay production access, Google Pay can fail at runtime with error code OR_BIBED_11.
Important: Complete the Google Pay production app registration and approval before going live. This requirement applies to production usage; test and sandbox flows may still work before production registration is finalized.
| Resource | Description |
|---|---|
| API Reference | Full API documentation for session creation and payment flows. |
| Example App | Sample Jetpack Compose app demonstrating SDK integration. |
See PRIVACY.md for a full breakdown of data collected, how it is used, whether it is linked to the end user's identity, and whether it is used for tracking — formatted for Google Play Data Safety.
Xendit Components Android is available under the Apache License 2.0. See the LICENSE file for more info.