Skip to content

Quickstart

Follow our walkthrough to get Checkout Drop-in running in minutes.

Pre-requisites

Before you start, make sure you have:

  • Android Studio installed on your computer
  • Android SDK 24 or higher
  • Kotlin 2.2.10 or higher
  • Your API credentials from the Unity Portal

Add the SDK dependency

To get started, add the latest version of the Android SDK to your project's build.gradle file.

dependencies {
    implementation("com.pxpfinancial:android-components-sdk:1.0.0")
}

The SDK automatically includes the required Android permissions in its manifest (INTERNET, ACCESS_NETWORK_STATE, and SYSTEM_ALERT_WINDOW). These permissions are necessary for payment processing, network connectivity checks, and 3DS authentication challenges.

Create a session on your backend

Drop-in needs a session from the PXP API. This must happen on your backend using HMAC authentication.

kotlin
dotnet

Store your credentials securely

Set up your API credentials as environment variables — never hardcode them in your application.

Create the HMAC signature function

This function generates a secure authentication hash by combining the timestamp, request ID, request path, and request body, then hashing with your token value using HMAC SHA256.

Build the session request body

Create a request with your merchant details and transaction information. The request body must be minified (no whitespace) for the HMAC signature.

Send the session creation request

POST to https://api-services.pxp.io/api/v1/sessions with your authentication headers and request body.

Return the session data to your app

The API returns sessionId, hmacKey, data, encryptionKey, locale, and allowedFundingTypes. Pass this entire response to your Android app.

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.security.MessageDigest
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

// {% step id="credentials-setup" %}
// Store your credentials securely
val CLIENT_ID = System.getenv("PXP_CLIENT_ID") ?: throw Exception("PXP_CLIENT_ID not set")
val TOKEN_ID = System.getenv("PXP_TOKEN_ID") ?: throw Exception("PXP_TOKEN_ID not set")
val TOKEN_VALUE = System.getenv("PXP_TOKEN_VALUE") ?: throw Exception("PXP_TOKEN_VALUE not set")
// {% /step %}

// {% step id="create-signature-function" %}
fun createHmacSignature(
    timestamp: Long,
    requestId: String,
    requestPath: String,
    requestBody: String,
    tokenValue: String
): String {
    val message = "$timestamp$requestId$requestPath$requestBody"
    val mac = Mac.getInstance("HmacSHA256")
    val secretKey = SecretKeySpec(tokenValue.toByteArray(), "HmacSHA256")
    mac.init(secretKey)
    val hash = mac.doFinal(message.toByteArray())
    return hash.joinToString("") { "%02x".format(it) }
}
// {% /step %}

@Serializable
data class SessionRequest(
    val merchant: String,
    val site: String,
    val sessionTimeout: Int,
    val merchantTransactionId: String,
    val transactionMethod: TransactionMethod,
    val amounts: Amounts,
    val allowTransaction: Boolean = true,
    val serviceType: String = "CheckoutDropIn"
)

@Serializable
data class TransactionMethod(
    val intent: Intent
)

@Serializable
data class Intent(
    val card: String,
    val paypal: String
)

@Serializable
data class Amounts(
    val currencyCode: String,
    val transactionValue: Double
)

@Serializable
data class SessionResponse(
    val sessionId: String,
    val hmacKey: String,
    val data: String,
    val encryptionKey: String,
    val locale: String,
    val allowedFundingTypes: Map<String, Any>
)

suspend fun createSession(): SessionResponse {
    val client = HttpClient()
    
    // {% step id="build-request-body" %}
    val sessionRequest = SessionRequest(
        merchant = "MERCHANT-1",
        site = "SITE-1",
        sessionTimeout = 120,
        merchantTransactionId = UUID.randomUUID().toString(),
        transactionMethod = TransactionMethod(
            intent = Intent(
                card = "Authorisation",
                paypal = "Purchase"
            )
        ),
        amounts = Amounts(
            currencyCode = "USD",
            transactionValue = 25.00
        )
    )
    
    // Minify the JSON (no whitespace)
    val requestBody = Json.encodeToString(sessionRequest)
    // {% /step %}
    
    // {% step id="send-session-request" %}
    val timestamp = System.currentTimeMillis()
    val requestId = UUID.randomUUID().toString()
    val requestPath = "api/v1/sessions"
    
    val hmac = createHmacSignature(
        timestamp = timestamp,
        requestId = requestId,
        requestPath = requestPath,
        requestBody = requestBody,
        tokenValue = TOKEN_VALUE
    )
    
    val authHeader = "PXP-UST1 $TOKEN_ID:$timestamp:$hmac"
    
    val response: HttpResponse = client.post("https://api-services.pxp.io/api/v1/sessions") {
        contentType(ContentType.Application.Json)
        headers {
            append("X-Client-Id", CLIENT_ID)
            append("X-Request-Id", requestId)
            append("Authorization", authHeader)
        }
        setBody(requestBody)
    }
    // {% /step %}
    
    // {% step id="handle-response" %}
    val sessionData = Json.decodeFromString<SessionResponse>(response.bodyAsText())
    return sessionData
    // {% /step %}
}

Initialise Drop-in in your app

Import the required dependencies

Import CheckoutDropIn, CheckoutDropInConfig, and the necessary types from the Android SDK.

Set up your Compose activity

Create a Jetpack Compose activity that will host the Checkout Drop-in interface.

Fetch the session data from your backend

Call your backend endpoint to get the session data you created in the previous steps.

Initialise Checkout Drop-in

Configure Drop-in with your environment, session data, and transaction details.

Configure the transaction data

Specify the currency, amount, entry type, and payment intents for each payment method.

Provide a shopper identifier

Implement the onGetShopper callback to provide shopper information. This enables Card-on-File functionality for saved cards.

Handle successful payments

Implement the onSuccess callback to handle successful payments. Always verify payments on your backend before fulfilling orders — frontend callbacks can be manipulated.

Handle payment errors

Implement the onError callback to handle payment failures and display appropriate error messages.

Create the Drop-in component

Call the create() method from a coroutine to generate the composable component.

Render the Drop-in content

Call the Content() composable to display the payment interface in your Compose UI.

// {% step id="import-dependencies" %}
package com.example.checkout

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.pxp.checkout.checkoutdropin.CheckoutDropIn
import com.pxp.checkout.checkoutdropin.types.CheckoutDropInConfig
import com.pxp.checkout.checkoutdropin.types.SessionConfig
import com.pxp.checkout.checkoutdropin.types.Shopper
import com.pxp.checkout.checkoutdropin.types.DropInPayPalIntentType
import com.pxp.checkout.models.DropInTransactionData
import com.pxp.checkout.models.DropInTransactionIntentData
import com.pxp.checkout.models.Environment
import com.pxp.checkout.models.EntryType
import com.pxp.checkout.models.IntentType
import com.pxp.checkout.services.models.transaction.Shopper
import com.pxp.checkout.components.checkoutdropincomponent.CheckoutDropInComponent
import kotlinx.coroutines.launch
import java.time.Instant
import java.util.UUID
// {% /step %}

// {% step id="setup-activity" %}
class CheckoutActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CheckoutScreen()
                }
            }
        }
    }
}
// {% /step %}

@Composable
fun CheckoutScreen() {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()
    var checkoutDropInComponent by remember { mutableStateOf<CheckoutDropInComponent?>(null) }
    var isLoading by remember { mutableStateOf(true) }
    var sessionData by remember { mutableStateOf<SessionData?>(null) }

    // {% step id="fetch-session" %}
    LaunchedEffect(Unit) {
        // Fetch session from your backend
        sessionData = fetchSessionFromBackend()
    }
    // {% /step %}

    LaunchedEffect(sessionData) {
        sessionData?.let { data ->
            // {% step id="initialize-dropin" %}
            val checkoutDropIn = CheckoutDropIn.initialize(
                context = context,
                config = CheckoutDropInConfig(
                    environment = Environment.TEST,
                    session = sessionData,
                    ownerType = "MerchantGroup",
                    ownerId = "MERCHANT-1",
                    // {% step id="configure-transaction" %}
                    transactionData = DropInTransactionData(
                        currency = "USD",
                        amount = 25.00,
                        entryType = EntryType.Ecom,
                        intent = DropInTransactionIntentData(
                            card = IntentType.Authorisation,
                            paypalDropInIntent = DropInPayPalIntentType.Authorisation
                        ),
                        merchant = "MERCHANT-1",
                        merchantTransactionId = UUID.randomUUID().toString(),
                        merchantTransactionDate = { Instant.now().toString() }
                    ),
                    // {% /step %}
                    // {% step id="setup-shopper" %}
                    onGetShopper = {
                        // Return your shopper identifier
                        Shopper(id = "shopper-${System.currentTimeMillis()}")
                    },
                    // {% /step %}
                    // {% step id="success-callback" %}
                    onSuccess = { transactionResult ->
                        // Payment succeeded
                        println("Payment successful: ${transactionResult.systemTransactionId}")
                        // IMPORTANT: Always verify on your backend before fulfilling orders
                    },
                    // {% /step %}
                    // {% step id="error-callback" %}
                    onError = { error ->
                        // Handle payment error
                        println("Payment failed: ${error.message}")
                    }
                    // {% /step %}
                )
            )
            // {% /step %}

            // {% step id="create-dropin" %}
            coroutineScope.launch {
                checkoutDropInComponent = checkoutDropIn.create()
                isLoading = false
            }
            // {% /step %}
        }
    }

    // {% step id="render-content" %}
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        if (isLoading) {
            CircularProgressIndicator()
        } else {
            checkoutDropInComponent?.Content(modifier = Modifier.fillMaxWidth())
        }
    }
    // {% /step %}
}

data class SessionData(
    val sessionId: String,
    val hmacKey: String,
    val data: String,
    val encryptionKey: String,
    val locale: String
)

suspend fun fetchSessionFromBackend(): SessionData {
    // Call your backend endpoint that creates a session
    // This is a placeholder - implement your actual backend call
    return SessionData(
        sessionId = "your-session-id",
        hmacKey = "your-hmac-key",
        data = "your-session-data",
        encryptionKey = "your-encryption-key",
        locale = "en-US"
    )
}
kotlin
dotnet

Verify payments

When a payment succeeds, the onSuccess callback fires with transaction details. However, you must always verify the payment on your backend before fulfilling orders.

Configure webhooks in the Unity Portal to receive real-time payment notifications on your backend.

Create the webhook endpoint

Set up an endpoint at /webhooks/pxp to receive payment notifications from Unity.

Process webhook events

Loop through the events array and filter for Transaction events.

Check payment state

Verify the transaction state is Authorised or Captured before processing.

Prevent duplicate processing

Check if you've already processed this transaction using systemTransactionId.

Verify transaction details

Match the merchantTransactionId, amount, and currency against your order records.

Fulfil the order

If verification passes, fulfil the order and mark the transaction as processed.

Respond to webhook

Always return { state: 'Success' } to acknowledge receipt, even if processing failed.

You can also verify payments using the Transactions API to query transaction status directly. See the Implementation guide for details.

import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

// {% step id="webhook-endpoint" %}
fun Application.configureWebhooks() {
    routing {
        post("/webhooks/pxp") {
            val webhookPayload = call.receive<WebhookPayload>()
            
            // {% step id="process-events" %}
            webhookPayload.events.forEach { event ->
                if (event.type == "Transaction") {
                    val transaction = event.data as? TransactionData ?: return@forEach
                    
                    // {% step id="check-state" %}
                    if (transaction.state in listOf("Authorised", "Captured")) {
                        // {% /step %}
                        
                        // {% step id="prevent-duplicates" %}
                        val alreadyProcessed = checkIfProcessed(transaction.systemTransactionId)
                        if (alreadyProcessed) {
                            return@forEach
                        }
                        // {% /step %}
                        
                        // {% step id="verify-details" %}
                        val order = getOrderByMerchantTransactionId(transaction.merchantTransactionId)
                        if (order != null &&
                            order.amount == transaction.amount &&
                            order.currency == transaction.currency) {
                            // {% /step %}
                            
                            // {% step id="fulfill-order" %}
                            fulfillOrder(order.id)
                            markTransactionAsProcessed(transaction.systemTransactionId)
                            // {% /step %}
                        }
                    }
                }
            }
            // {% /step %}
            
            // {% step id="respond" %}
            call.respond(mapOf("state" to "Success"))
            // {% /step %}
        }
    }
}
// {% /step %}

@Serializable
data class WebhookPayload(
    val events: List<WebhookEvent>
)

@Serializable
data class WebhookEvent(
    val type: String,
    val data: TransactionData
)

@Serializable
data class TransactionData(
    val systemTransactionId: String,
    val merchantTransactionId: String,
    val state: String,
    val amount: Double,
    val currency: String
)

data class Order(
    val id: String,
    val merchantTransactionId: String,
    val amount: Double,
    val currency: String
)

// Placeholder functions - implement these according to your database
fun checkIfProcessed(systemTransactionId: String): Boolean {
    // Check your database to see if this transaction was already processed
    return false
}

fun getOrderByMerchantTransactionId(merchantTransactionId: String): Order? {
    // Retrieve order from your database
    return null
}

fun fulfillOrder(orderId: String) {
    // Fulfill the order (ship product, grant access, etc.)
}

fun markTransactionAsProcessed(systemTransactionId: String) {
    // Mark transaction as processed in your database to prevent duplicates
}

That's it! You now have a working Checkout Drop-in integration.

What's next?

Now that you have Drop-in running, here are the recommended next steps: